From 9dc77aebf79f0200ab73ab2e1ea79c2d49f2f76a Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 2 Apr 2026 04:44:04 +0200 Subject: [PATCH 1/8] feat: add advanced wired variable system and tooling --- Database Updates/000_all_database_updates.sql | 4 +- ...ed_settings_to_wired_emulator_settings.sql | 98 ++ ...dd_comment_column_to_emulator_settings.sql | 332 ++++ .../004_normalize_permissions_schema.sql | 499 ++++++ .../005_add_room_wired_settings.sql | 7 + .../006_add_room_user_wired_variables.sql | 33 + .../007_add_wired_variable_timestamps.sql | 32 + .../eu/habbo/core/ConfigurationManager.java | 134 +- .../habbohotel/commands/WiredCommand.java | 11 +- .../habbo/habbohotel/items/ItemManager.java | 34 + .../InteractionWiredCondition.java | 2 +- .../interactions/InteractionWiredEffect.java | 2 +- .../interactions/InteractionWiredExtra.java | 2 +- .../interactions/InteractionWiredTrigger.java | 2 +- .../conditions/WiredConditionHasVariable.java | 506 ++++++ .../WiredConditionNotHasVariable.java | 30 + .../WiredConditionVariableAgeMatch.java | 420 +++++ .../WiredConditionVariableValueMatch.java | 814 ++++++++++ .../WiredEffectChangeVariableValue.java | 932 +++++++++++ .../effects/WiredEffectGiveVariable.java | 434 +++++ .../effects/WiredEffectRemoveVariable.java | 370 +++++ .../wired/effects/WiredEffectSendSignal.java | 7 +- .../extra/WiredExtraContextVariable.java | 132 ++ .../WiredExtraFilterFurniByVariable.java | 28 + .../WiredExtraFilterUsersByVariable.java | 28 + .../wired/extra/WiredExtraFurniVariable.java | 157 ++ .../wired/extra/WiredExtraRoomVariable.java | 159 ++ .../extra/WiredExtraTextInputVariable.java | 271 ++++ .../extra/WiredExtraTextOutputVariable.java | 544 +++++++ .../wired/extra/WiredExtraUserVariable.java | 162 ++ .../wired/extra/WiredExtraVariableEcho.java | 800 +++++++++ .../extra/WiredExtraVariableFilterBase.java | 757 +++++++++ .../WiredExtraVariableLevelUpSystem.java | 270 ++++ .../extra/WiredExtraVariableReference.java | 321 ++++ .../WiredExtraVariableTextConnector.java | 204 +++ .../extra/WiredVariableNameValidator.java | 110 ++ .../extra/WiredVariableReferenceSupport.java | 629 ++++++++ .../WiredEffectFurniWithVariable.java | 29 + .../WiredEffectUsersWithVariable.java | 29 + .../WiredEffectVariableSelectorBase.java | 862 ++++++++++ .../WiredTriggerHabboSaysKeyword.java | 12 + .../triggers/WiredTriggerVariableChanged.java | 302 ++++ .../habbohotel/modtool/ModToolManager.java | 16 +- .../permissions/PermissionsManager.java | 164 +- .../eu/habbo/habbohotel/permissions/Rank.java | 74 +- .../com/eu/habbo/habbohotel/rooms/Room.java | 235 ++- .../habbohotel/rooms/RoomChatManager.java | 11 +- .../rooms/RoomFurniVariableManager.java | 1001 ++++++++++++ .../habbohotel/rooms/RoomItemManager.java | 68 +- .../habbo/habbohotel/rooms/RoomManager.java | 1 + .../habbohotel/rooms/RoomUnitManager.java | 12 +- .../rooms/RoomUserVariableManager.java | 1101 +++++++++++++ .../habbohotel/rooms/RoomVariableManager.java | 827 ++++++++++ .../rooms/WiredVariableDefinitionInfo.java | 43 + .../habbohotel/wired/WiredConditionType.java | 6 +- .../habbohotel/wired/WiredEffectType.java | 7 +- .../habbohotel/wired/WiredTriggerType.java | 1 + .../habbohotel/wired/core/WiredContext.java | 14 + .../wired/core/WiredContextVariableScope.java | 134 ++ .../core/WiredContextVariableSupport.java | 152 ++ .../habbohotel/wired/core/WiredEngine.java | 440 ++++- .../habbohotel/wired/core/WiredEvent.java | 130 ++ .../core/WiredInternalVariableSupport.java | 554 +++++++ .../habbohotel/wired/core/WiredManager.java | 118 +- .../wired/core/WiredMoveCarryHelper.java | 20 +- .../wired/core/WiredRoomDiagnostics.java | 586 +++++++ .../wired/core/WiredSourceUtil.java | 39 +- .../core/WiredTextInputCaptureSupport.java | 246 +++ .../wired/core/WiredTextPlaceholderUtil.java | 335 +++- .../wired/core/WiredTriggerSourceUtil.java | 39 +- .../core/WiredVariableLevelSystemSupport.java | 564 +++++++ .../WiredVariableTextConnectorSupport.java | 61 + .../habbohotel/wired/migrate/WiredEvents.java | 42 + .../com/eu/habbo/messages/PacketManager.java | 12 + .../eu/habbo/messages/incoming/Incoming.java | 6 + .../wired/WiredApplySetConditionsEvent.java | 2 +- .../wired/WiredConditionSaveDataEvent.java | 3 +- .../wired/WiredEffectSaveDataEvent.java | 3 +- .../wired/WiredMonitorRequestEvent.java | 41 + .../wired/WiredRoomSettingsRequestEvent.java | 23 + .../wired/WiredRoomSettingsSaveEvent.java | 38 + .../wired/WiredTriggerSaveDataEvent.java | 3 +- .../wired/WiredUserVariableManageEvent.java | 74 + .../wired/WiredUserVariableUpdateEvent.java | 53 + .../wired/WiredUserVariablesRequestEvent.java | 22 + .../eu/habbo/messages/outgoing/Outgoing.java | 3 + .../modtool/ModToolUserInfoComposer.java | 6 +- .../rooms/WiredMovementsComposer.java | 109 +- .../wired/WiredMonitorDataComposer.java | 84 + .../wired/WiredRoomSettingsDataComposer.java | 38 + .../wired/WiredUserVariablesDataComposer.java | 171 ++ .../com/eu/habbo/plugin/PluginManager.java | 14 + docs/emulator_settings_reference.md | 781 +++++++++ docs/permissions_schema_reference.md | 187 +++ docs/wired_bug_audit.md | 382 +++++ docs/wired_full_reference.html | 500 ++++++ docs/wired_full_reference.md | 1429 +++++++++++++++++ docs/wired_tools_implementation_summary.md | 283 ++++ docs/wired_tools_reference.md | 554 +++++++ 99 files changed, 22169 insertions(+), 204 deletions(-) create mode 100644 Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql create mode 100644 Database Updates/003_add_comment_column_to_emulator_settings.sql create mode 100644 Database Updates/004_normalize_permissions_schema.sql create mode 100644 Database Updates/005_add_room_wired_settings.sql create mode 100644 Database Updates/006_add_room_user_wired_variables.sql create mode 100644 Database Updates/007_add_wired_variable_timestamps.sql create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHasVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraContextVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurniByVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUsersByVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFurniVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRoomVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUserVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableFilterBase.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableLevelUpSystem.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableReference.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniWithVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersWithVariable.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerVariableChanged.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/WiredVariableDefinitionInfo.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableScope.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableLevelSystemSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredMonitorRequestEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsRequestEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsSaveEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableManageEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableUpdateEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariablesRequestEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredRoomSettingsDataComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredUserVariablesDataComposer.java create mode 100644 docs/emulator_settings_reference.md create mode 100644 docs/permissions_schema_reference.md create mode 100644 docs/wired_bug_audit.md create mode 100644 docs/wired_full_reference.html create mode 100644 docs/wired_full_reference.md create mode 100644 docs/wired_tools_implementation_summary.md create mode 100644 docs/wired_tools_reference.md diff --git a/Database Updates/000_all_database_updates.sql b/Database Updates/000_all_database_updates.sql index c4b91c37..53ee1db2 100644 --- a/Database Updates/000_all_database_updates.sql +++ b/Database Updates/000_all_database_updates.sql @@ -213,8 +213,8 @@ ON DUPLICATE KEY UPDATE `key` = `key`; -- Wired engine configuration INSERT INTO `emulator_settings` (`key`, `value`) VALUES -('wired.engine.enabled', '0'), -('wired.engine.exclusive', '0'), +('wired.engine.enabled', '1'), +('wired.engine.exclusive', '1'), ('wired.engine.maxStepsPerStack', '100'), ('wired.engine.debug', '0') ON DUPLICATE KEY UPDATE `key` = `key`; diff --git a/Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql b/Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql new file mode 100644 index 00000000..a95755a3 --- /dev/null +++ b/Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql @@ -0,0 +1,98 @@ +CREATE TABLE IF NOT EXISTS `wired_emulator_settings` ( + `key` varchar(191) NOT NULL, + `value` text NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) +SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.' +UNION ALL +SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.' +UNION ALL +SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.' +UNION ALL +SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.' +UNION ALL +SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.' +UNION ALL +SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.' +UNION ALL +SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.' +UNION ALL +SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.' +UNION ALL +SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.' +UNION ALL +SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.' +UNION ALL +SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.' +UNION ALL +SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.' +UNION ALL +SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.' +UNION ALL +SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.' +UNION ALL +SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.' +UNION ALL +SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.' +UNION ALL +SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.' +UNION ALL +SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.' +UNION ALL +SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.' +UNION ALL +SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.' +UNION ALL +SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.' +UNION ALL +SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.' +UNION ALL +SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.' +UNION ALL +SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.' +UNION ALL +SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.' +UNION ALL +SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.' +UNION ALL +SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.' +UNION ALL +SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.' +ON DUPLICATE KEY UPDATE + `value` = VALUES(`value`), + `comment` = VALUES(`comment`); + +DELETE FROM `emulator_settings` +WHERE `key` IN ( + 'wired.engine.enabled', + 'wired.engine.exclusive', + 'wired.engine.maxStepsPerStack', + 'wired.engine.debug', + 'wired.custom.enabled', + 'hotel.wired.furni.selection.count', + 'hotel.wired.max_delay', + 'hotel.wired.message.max_length', + 'wired.effect.teleport.delay', + 'wired.place.under', + 'wired.tick.interval.ms', + 'wired.tick.resolution', + 'wired.tick.debug', + 'wired.tick.thread.priority', + 'wired.highscores.displaycount', + 'wired.abuse.max.recursion.depth', + 'wired.abuse.max.events.per.window', + 'wired.abuse.rate.limit.window.ms', + 'wired.abuse.ban.duration.ms', + 'wired.monitor.usage.window.ms', + 'wired.monitor.usage.limit', + 'wired.monitor.delayed.events.limit', + 'wired.monitor.overload.average.ms', + 'wired.monitor.overload.peak.ms', + 'wired.monitor.overload.consecutive.windows', + 'wired.monitor.heavy.usage.percent', + 'wired.monitor.heavy.consecutive.windows', + 'wired.monitor.heavy.delayed.percent' +); diff --git a/Database Updates/003_add_comment_column_to_emulator_settings.sql b/Database Updates/003_add_comment_column_to_emulator_settings.sql new file mode 100644 index 00000000..63e70a94 --- /dev/null +++ b/Database Updates/003_add_comment_column_to_emulator_settings.sql @@ -0,0 +1,332 @@ +ALTER TABLE `emulator_settings` + ADD COLUMN IF NOT EXISTS `comment` text NOT NULL AFTER `value`; + +UPDATE `emulator_settings` SET `comment` = 'Characters allowed when users choose or change a username.' WHERE `key` = 'allowed.username.characters'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in milliseconds used by the Apollyon-specific behaviour or command flow.' WHERE `key` = 'apollyon.cooldown.amount'; +UPDATE `emulator_settings` SET `comment` = 'Asset URL used by the BaseJump or FastFood game client.' WHERE `key` = 'basejump.assets.url'; +UPDATE `emulator_settings` SET `comment` = 'SWF URL used to launch the BaseJump or FastFood game client.' WHERE `key` = 'basejump.url'; +UPDATE `emulator_settings` SET `comment` = 'Date format used by visitor bots when they print timestamps.' WHERE `key` = 'bots.visitor.dateformat'; +UPDATE `emulator_settings` SET `comment` = 'Master switch for bubble alert notifications.' WHERE `key` = 'bubblealerts.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Enable bubble alerts when friends come online.' WHERE `key` = 'bubblealerts.notif_friendonline.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Image template used when showing friend-online bubble alerts.' WHERE `key` = 'bubblealerts.notif_friendonline.image'; +UPDATE `emulator_settings` SET `comment` = 'Use the configured figure image inside friend-online bubble alerts.' WHERE `key` = 'bubblealerts.notif_friendonline.useimage'; +UPDATE `emulator_settings` SET `comment` = 'Show bubble alerts for marketplace notifications.' WHERE `key` = 'bubblealerts.notif_marketplace.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Show bubble alerts for limited-item purchases.' WHERE `key` = 'bubblealerts.notif_purchase.limited'; +UPDATE `emulator_settings` SET `comment` = 'Allow bots to be included in room bundles or package rewards.' WHERE `key` = 'bundle.bots.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Allow pets to be included in room bundles or package rewards.' WHERE `key` = 'bundle.pets.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Enable the GET callback used to report version to external services.' WHERE `key` = 'callback.get.version'; +UPDATE `emulator_settings` SET `comment` = 'Enable the POST callback used to report errors to external services.' WHERE `key` = 'callback.post.errors'; +UPDATE `emulator_settings` SET `comment` = 'Enable the POST callback used to report statistics to external services.' WHERE `key` = 'callback.post.statistics'; +UPDATE `emulator_settings` SET `comment` = 'Enable the in-room camera feature.' WHERE `key` = 'camera.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Extradata template written into camera photo items when they are created.' WHERE `key` = 'camera.extradata'; +UPDATE `emulator_settings` SET `comment` = 'Base item ID used by the generated camera photo furniture.' WHERE `key` = 'camera.item_id'; +UPDATE `emulator_settings` SET `comment` = 'Credit price charged when taking a camera photo.' WHERE `key` = 'camera.price.credits'; +UPDATE `emulator_settings` SET `comment` = 'Amount of activity points charged when taking a camera photo.' WHERE `key` = 'camera.price.points'; +UPDATE `emulator_settings` SET `comment` = 'Amount of activity points charged when publishing a camera photo.' WHERE `key` = 'camera.price.points.publish'; +UPDATE `emulator_settings` SET `comment` = 'Activity point type used for the camera publish cost.' WHERE `key` = 'camera.price.points.publish.type'; +UPDATE `emulator_settings` SET `comment` = 'Activity point type used for the camera capture cost.' WHERE `key` = 'camera.price.points.type'; +UPDATE `emulator_settings` SET `comment` = 'Delay in seconds before a published camera photo becomes available.' WHERE `key` = 'camera.publish.delay'; +UPDATE `emulator_settings` SET `comment` = 'Base URL where camera images are published.' WHERE `key` = 'camera.url'; +UPDATE `emulator_settings` SET `comment` = 'Force HTTPS when generating camera image URLs.' WHERE `key` = 'camera.use.https'; +UPDATE `emulator_settings` SET `comment` = 'Require HC or VIP status before users can create a guild.' WHERE `key` = 'catalog.guild.hc_required'; +UPDATE `emulator_settings` SET `comment` = 'Credit cost required to create a guild.' WHERE `key` = 'catalog.guild.price'; +UPDATE `emulator_settings` SET `comment` = 'Layout or image ID used when a limited page is sold out.' WHERE `key` = 'catalog.ltd.page.soldout'; +UPDATE `emulator_settings` SET `comment` = 'Randomize the order or selection of limited catalog items.' WHERE `key` = 'catalog.ltd.random'; +UPDATE `emulator_settings` SET `comment` = 'Catalog page ID used for VIP gift redemption.' WHERE `key` = 'catalog.page.vipgifts'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of chat color IDs blocked for the chatcolor command.' WHERE `key` = 'commands.cmd_chatcolor.banned_numbers'; +UPDATE `emulator_settings` SET `comment` = 'Minimum permission rank required to use the staffonline command.' WHERE `key` = 'commands.cmd_staffonline.min_rank'; +UPDATE `emulator_settings` SET `comment` = 'Use the legacy command plugin loading style.' WHERE `key` = 'commands.plugins.oldstyle'; +UPDATE `emulator_settings` SET `comment` = 'Controls the emulator console mode or console output style.' WHERE `key` = 'console.mode'; +UPDATE `emulator_settings` SET `comment` = 'Enable custom item stacking behaviour outside the default stacking rules.' WHERE `key` = 'custom.stacking.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum batch or partition size used by partitioned database operations.' WHERE `key` = 'db.max.partition.size'; +UPDATE `emulator_settings` SET `comment` = 'Minimum batch or partition size used by partitioned database operations.' WHERE `key` = 'db.min.partition.size'; +UPDATE `emulator_settings` SET `comment` = 'Maximum size of the database connection pool.' WHERE `key` = 'db.pool.maxsize'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of open connections kept in the database pool.' WHERE `key` = 'db.pool.minsize'; +UPDATE `emulator_settings` SET `comment` = 'Enable general emulator debug mode.' WHERE `key` = 'debug.mode'; +UPDATE `emulator_settings` SET `comment` = 'Show internal debug error messages.' WHERE `key` = 'debug.show.errors'; +UPDATE `emulator_settings` SET `comment` = 'Show packet headers in debug logs.' WHERE `key` = 'debug.show.headers'; +UPDATE `emulator_settings` SET `comment` = 'Print packet-level debug output.' WHERE `key` = 'debug.show.packets'; +UPDATE `emulator_settings` SET `comment` = 'Print debug output for undefined incoming or outgoing packets.' WHERE `key` = 'debug.show.packets.undefined'; +UPDATE `emulator_settings` SET `comment` = 'Log SQL exceptions to the console.' WHERE `key` = 'debug.show.sql.exception'; +UPDATE `emulator_settings` SET `comment` = 'Show user-related debug messages.' WHERE `key` = 'debug.show.users'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated discount thresholds used for extra batch bonuses.' WHERE `key` = 'discount.additional.thresholds'; +UPDATE `emulator_settings` SET `comment` = 'Number of free items granted inside one discount batch.' WHERE `key` = 'discount.batch.free.items'; +UPDATE `emulator_settings` SET `comment` = 'Number of items required for one discount batch.' WHERE `key` = 'discount.batch.size'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of discount batches required before the bonus logic applies.' WHERE `key` = 'discount.bonus.min.discounts'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of catalog items that can participate in one discount batch.' WHERE `key` = 'discount.max.allowed.items'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `easter_eggs.enabled`.' WHERE `key` = 'easter_eggs.enabled'; +UPDATE `emulator_settings` SET `comment` = 'RSA private exponent used by the encryption layer.' WHERE `key` = 'enc.d'; +UPDATE `emulator_settings` SET `comment` = 'RSA public exponent used by the encryption layer.' WHERE `key` = 'enc.e'; +UPDATE `emulator_settings` SET `comment` = 'Enable RSA encryption support for the socket handshake.' WHERE `key` = 'enc.enabled'; +UPDATE `emulator_settings` SET `comment` = 'RSA modulus used by the encryption layer.' WHERE `key` = 'enc.n'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated effect IDs used by the kill command for the killer.' WHERE `key` = 'essentials.cmd_kill.effect.killer'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated effect IDs used by the kill command for the victim.' WHERE `key` = 'essentials.cmd_kill.effect.victim'; +UPDATE `emulator_settings` SET `comment` = 'Allow users with room rights to bypass the normal flood protection.' WHERE `key` = 'flood.with.rights'; +UPDATE `emulator_settings` SET `comment` = 'Enable FTP uploads for generated assets.' WHERE `key` = 'ftp.enabled'; +UPDATE `emulator_settings` SET `comment` = 'FTP host used for asset uploads.' WHERE `key` = 'ftp.host'; +UPDATE `emulator_settings` SET `comment` = 'FTP password used for asset uploads.' WHERE `key` = 'ftp.password'; +UPDATE `emulator_settings` SET `comment` = 'FTP username used for asset uploads.' WHERE `key` = 'ftp.user'; +UPDATE `emulator_settings` SET `comment` = 'Maximum tile distance at which talking furniture can react to nearby speech.' WHERE `key` = 'furniture.talking.range'; +UPDATE `emulator_settings` SET `comment` = 'API key used by the FastFood or BaseJump integration.' WHERE `key` = 'gamecenter.fastfood.apiKey'; +UPDATE `emulator_settings` SET `comment` = 'Asset base URL used by the FastFood or BaseJump game client.' WHERE `key` = 'gamecenter.fastfood.assets'; +UPDATE `emulator_settings` SET `comment` = 'Background color used by the FastFood launcher UI.' WHERE `key` = 'gamecenter.fastfood.background.color'; +UPDATE `emulator_settings` SET `comment` = 'Enable the FastFood or BaseJump gamecenter integration.' WHERE `key` = 'gamecenter.fastfood.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Text color used by the FastFood launcher UI.' WHERE `key` = 'gamecenter.fastfood.text.color'; +UPDATE `emulator_settings` SET `comment` = 'Theme name used by the FastFood launcher.' WHERE `key` = 'gamecenter.fastfood.theme'; +UPDATE `emulator_settings` SET `comment` = 'Background image used for the SnowWar Arctic map.' WHERE `key` = 'gamecenter.snowwar.artic.bg'; +UPDATE `emulator_settings` SET `comment` = 'Asset base URL used by the SnowWar game client.' WHERE `key` = 'gamecenter.snowwar.assets'; +UPDATE `emulator_settings` SET `comment` = 'Background image used for the SnowWar Dragon Cave map.' WHERE `key` = 'gamecenter.snowwar.dragoncave.bg'; +UPDATE `emulator_settings` SET `comment` = 'Enable the SnowWar gamecenter integration.' WHERE `key` = 'gamecenter.snowwar.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Background image used for the SnowWar Fight Night map.' WHERE `key` = 'gamecenter.snowwar.fightnight.bg'; +UPDATE `emulator_settings` SET `comment` = 'Background color used by the SnowWar launcher UI.' WHERE `key` = 'gamecenter.snowwar.game.background.color'; +UPDATE `emulator_settings` SET `comment` = 'Countdown in seconds before a SnowWar round starts.' WHERE `key` = 'gamecenter.snowwar.game.start.time'; +UPDATE `emulator_settings` SET `comment` = 'Text color used by the SnowWar launcher UI.' WHERE `key` = 'gamecenter.snowwar.game.text.color'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of players required to start SnowWar.' WHERE `key` = 'gamecenter.snowwar.players.min'; +UPDATE `emulator_settings` SET `comment` = 'Room ID used as the SnowWar lobby or host room.' WHERE `key` = 'gamecenter.snowwar.room.id'; +UPDATE `emulator_settings` SET `comment` = 'Remote figuredata URL used when the hotel loads avatar figure definitions.' WHERE `key` = 'gamedata.figuredata.url'; +UPDATE `emulator_settings` SET `comment` = 'Time in seconds that guardians have to accept a case.' WHERE `key` = 'guardians.accept.timer'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of guardians that can be assigned to one case.' WHERE `key` = 'guardians.maximum.guardians.total'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of times an unanswered guardian case can be resent.' WHERE `key` = 'guardians.maximum.resends'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of guardian votes required to resolve a case.' WHERE `key` = 'guardians.minimum.votes'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in seconds before the same user can open a new guardian report.' WHERE `key` = 'guardians.reporting.cooldown'; +UPDATE `emulator_settings` SET `comment` = 'Use the legacy generic alert window style.' WHERE `key` = 'hotel.alert.oldstyle'; +UPDATE `emulator_settings` SET `comment` = 'Allow users to ignore staff accounts.' WHERE `key` = 'hotel.allow.ignore.staffs'; +UPDATE `emulator_settings` SET `comment` = 'Amount of credits granted on each automatic payout.' WHERE `key` = 'hotel.auto.credits.amount'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic credits payouts for HC users.' WHERE `key` = 'hotel.auto.credits.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.interval'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic gotwpoints payouts for HC users.' WHERE `key` = 'hotel.auto.gotwpoints.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.interval'; +UPDATE `emulator_settings` SET `comment` = 'Internal currency name used by the automatic gotwpoints payout.' WHERE `key` = 'hotel.auto.gotwpoints.name'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used by the automatic gotwpoints payout.' WHERE `key` = 'hotel.auto.gotwpoints.type'; +UPDATE `emulator_settings` SET `comment` = 'Amount of pixels granted on each automatic payout.' WHERE `key` = 'hotel.auto.pixels.amount'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic pixels payouts for HC users.' WHERE `key` = 'hotel.auto.pixels.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.interval'; +UPDATE `emulator_settings` SET `comment` = 'Amount of points granted on each automatic payout.' WHERE `key` = 'hotel.auto.points.amount'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic points payouts.' WHERE `key` = 'hotel.auto.points.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic points payouts for HC users.' WHERE `key` = 'hotel.auto.points.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic points payouts.' WHERE `key` = 'hotel.auto.points.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic points payouts.' WHERE `key` = 'hotel.auto.points.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic points payouts.' WHERE `key` = 'hotel.auto.points.interval'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.banzai.points.tile.fill`.' WHERE `key` = 'hotel.banzai.points.tile.fill'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.banzai.points.tile.lock`.' WHERE `key` = 'hotel.banzai.points.tile.lock'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.banzai.points.tile.steal`.' WHERE `key` = 'hotel.banzai.points.tile.steal'; +UPDATE `emulator_settings` SET `comment` = 'Maximum tile distance from which a butler bot accepts commands.' WHERE `key` = 'hotel.bot.butler.commanddistance'; +UPDATE `emulator_settings` SET `comment` = 'Maximum tile distance from which a butler bot can serve requests.' WHERE `key` = 'hotel.bot.butler.servedistance'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of seconds between bot chat lines.' WHERE `key` = 'hotel.bot.chat.minimum.interval'; +UPDATE `emulator_settings` SET `comment` = 'Maximum bot chat delay allowed when configuring scripted speech.' WHERE `key` = 'hotel.bot.max.chatdelay'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed in bot chat lines.' WHERE `key` = 'hotel.bot.max.chatlength'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed in bot names.' WHERE `key` = 'hotel.bot.max.namelength'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of bots allowed in one inventory.' WHERE `key` = 'hotel.bots.max.inventory'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of bots allowed in one room.' WHERE `key` = 'hotel.bots.max.room'; +UPDATE `emulator_settings` SET `comment` = 'Default calendar campaign name or identifier.' WHERE `key` = 'hotel.calendar.default'; +UPDATE `emulator_settings` SET `comment` = 'Enable the hotel calendar feature.' WHERE `key` = 'hotel.calendar.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to calendar pixel rewards for HC users.' WHERE `key` = 'hotel.calendar.pixels.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Unix timestamp used as the calendar start date.' WHERE `key` = 'hotel.calendar.starttimestamp'; +UPDATE `emulator_settings` SET `comment` = 'Number of discount slots or discount batches shown by the catalog.' WHERE `key` = 'hotel.catalog.discounts.amount'; +UPDATE `emulator_settings` SET `comment` = 'Respect catalog item order numbers when rendering pages.' WHERE `key` = 'hotel.catalog.items.display.ordernum'; +UPDATE `emulator_settings` SET `comment` = 'Enable daily purchase limits for limited catalog items.' WHERE `key` = 'hotel.catalog.ltd.limit.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in seconds between catalog purchases.' WHERE `key` = 'hotel.catalog.purchase.cooldown'; +UPDATE `emulator_settings` SET `comment` = 'Enable the catalog recycler feature.' WHERE `key` = 'hotel.catalog.recycler.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed in one public chat message.' WHERE `key` = 'hotel.chat.max.length'; +UPDATE `emulator_settings` SET `comment` = 'Daily amount of respect points available for users.' WHERE `key` = 'hotel.daily.respect'; +UPDATE `emulator_settings` SET `comment` = 'Daily amount of pet respect points available for users.' WHERE `key` = 'hotel.daily.respect.pets'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `hotel.ecotron.enabled`.' WHERE `key` = 'hotel.ecotron.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.1`.' WHERE `key` = 'hotel.ecotron.rarity.chance.1'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.2`.' WHERE `key` = 'hotel.ecotron.rarity.chance.2'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.3`.' WHERE `key` = 'hotel.ecotron.rarity.chance.3'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.4`.' WHERE `key` = 'hotel.ecotron.rarity.chance.4'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.5`.' WHERE `key` = 'hotel.ecotron.rarity.chance.5'; +UPDATE `emulator_settings` SET `comment` = 'Mute duration in seconds applied by the hotel flood protection.' WHERE `key` = 'hotel.flood.mute.time'; +UPDATE `emulator_settings` SET `comment` = 'Maximum total floorplan area allowed for custom rooms.' WHERE `key` = 'hotel.floorplan.max.totalarea'; +UPDATE `emulator_settings` SET `comment` = 'Maximum floorplan width or length allowed for custom rooms.' WHERE `key` = 'hotel.floorplan.max.widthlength'; +UPDATE `emulator_settings` SET `comment` = 'Number of explosion boosts lost when a player gets frozen.' WHERE `key` = 'hotel.freeze.onfreeze.loose.explosionboost'; +UPDATE `emulator_settings` SET `comment` = 'Number of snowballs lost when a player gets frozen.' WHERE `key` = 'hotel.freeze.onfreeze.loose.snowballs'; +UPDATE `emulator_settings` SET `comment` = 'Time in seconds a player remains frozen.' WHERE `key` = 'hotel.freeze.onfreeze.time.frozen'; +UPDATE `emulator_settings` SET `comment` = 'Score awarded for blocking tiles in Freeze.' WHERE `key` = 'hotel.freeze.points.block'; +UPDATE `emulator_settings` SET `comment` = 'Score awarded for using Freeze effects or power-up actions.' WHERE `key` = 'hotel.freeze.points.effect'; +UPDATE `emulator_settings` SET `comment` = 'Score awarded for freezing another player in Freeze.' WHERE `key` = 'hotel.freeze.points.freeze'; +UPDATE `emulator_settings` SET `comment` = 'Chance for Freeze power-ups to spawn.' WHERE `key` = 'hotel.freeze.powerup.chance'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of extra lives granted by a Freeze power-up.' WHERE `key` = 'hotel.freeze.powerup.max.lives'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of extra snowballs granted by a Freeze power-up.' WHERE `key` = 'hotel.freeze.powerup.max.snowballs'; +UPDATE `emulator_settings` SET `comment` = 'Allow Freeze protection power-ups to stack.' WHERE `key` = 'hotel.freeze.powerup.protection.stack'; +UPDATE `emulator_settings` SET `comment` = 'Protection time in seconds after receiving a Freeze protection power-up.' WHERE `key` = 'hotel.freeze.powerup.protection.time'; +UPDATE `emulator_settings` SET `comment` = 'Default friend category ID assigned to new friends.' WHERE `key` = 'hotel.friendcategory'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.achievement.olympics_c16_crosstrainer`.' WHERE `key` = 'hotel.furni.gym.achievement.olympics_c16_crosstrainer'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.achievement.olympics_c16_trampoline`.' WHERE `key` = 'hotel.furni.gym.achievement.olympics_c16_trampoline'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.achievement.olympics_c16_treadmill`.' WHERE `key` = 'hotel.furni.gym.achievement.olympics_c16_treadmill'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_crosstrainer`.' WHERE `key` = 'hotel.furni.gym.forcerot.olympics_c16_crosstrainer'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_trampoline`.' WHERE `key` = 'hotel.furni.gym.forcerot.olympics_c16_trampoline'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_treadmill`.' WHERE `key` = 'hotel.furni.gym.forcerot.olympics_c16_treadmill'; +UPDATE `emulator_settings` SET `comment` = 'Comma-separated list of gift box type IDs allowed in the catalog.' WHERE `key` = 'hotel.gifts.box_types'; +UPDATE `emulator_settings` SET `comment` = 'Maximum message length allowed on gift notes.' WHERE `key` = 'hotel.gifts.length.max'; +UPDATE `emulator_settings` SET `comment` = 'Comma-separated list of ribbon type IDs allowed in the catalog.' WHERE `key` = 'hotel.gifts.ribbon_types'; +UPDATE `emulator_settings` SET `comment` = 'Credit price used by special gift boxes.' WHERE `key` = 'hotel.gifts.special.price'; +UPDATE `emulator_settings` SET `comment` = 'Room ID used as the default home room for new users.' WHERE `key` = 'hotel.home.room'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of items allowed in one inventory.' WHERE `key` = 'hotel.inventory.max.items'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.hween14_rare2`.' WHERE `key` = 'hotel.item.trap.hween14_rare2'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.hween_c17_handstrap`.' WHERE `key` = 'hotel.item.trap.hween_c17_handstrap'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.hween_c17_spiketrap`.' WHERE `key` = 'hotel.item.trap.hween_c17_spiketrap'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.pirate_sandtrap`.' WHERE `key` = 'hotel.item.trap.pirate_sandtrap'; +UPDATE `emulator_settings` SET `comment` = 'Track limit used by large jukebox furniture.' WHERE `key` = 'hotel.jukebox.limit.large'; +UPDATE `emulator_settings` SET `comment` = 'Track limit used by normal jukebox furniture.' WHERE `key` = 'hotel.jukebox.limit.normal'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for chat.' WHERE `key` = 'hotel.log.chat'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for chat private.' WHERE `key` = 'hotel.log.chat.private'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for room enter.' WHERE `key` = 'hotel.log.room.enter'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for trades.' WHERE `key` = 'hotel.log.trades'; +UPDATE `emulator_settings` SET `comment` = 'Currency type used for marketplace prices and taxes.' WHERE `key` = 'hotel.marketplace.currency'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `hotel.marketplace.enabled`.' WHERE `key` = 'hotel.marketplace.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of bots allowed in one room.' WHERE `key` = 'hotel.max.bots.room'; +UPDATE `emulator_settings` SET `comment` = 'Maximum amount of duckets a user can hold.' WHERE `key` = 'hotel.max.duckets'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `hotel.messenger.offline.messaging.enabled`.' WHERE `key` = 'hotel.messenger.offline.messaging.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of results returned by messenger user searches.' WHERE `key` = 'hotel.messenger.search.maxresults'; +UPDATE `emulator_settings` SET `comment` = 'Public hotel name shown across the client and outgoing messages.' WHERE `key` = 'hotel.name'; +UPDATE `emulator_settings` SET `comment` = 'Enable navigator room previews or camera mode.' WHERE `key` = 'hotel.navigator.camera'; +UPDATE `emulator_settings` SET `comment` = 'Default owner name displayed by the navigator.' WHERE `key` = 'hotel.navigator.owner'; +UPDATE `emulator_settings` SET `comment` = 'Number of rooms shown in the popular rooms list.' WHERE `key` = 'hotel.navigator.popular.amount'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of rooms shown per popular category.' WHERE `key` = 'hotel.navigator.popular.category.maxresults'; +UPDATE `emulator_settings` SET `comment` = 'List type used for the popular rooms tab.' WHERE `key` = 'hotel.navigator.popular.listtype'; +UPDATE `emulator_settings` SET `comment` = 'Include public rooms inside the popular rooms tab.' WHERE `key` = 'hotel.navigator.populartab.publics'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of results returned by navigator searches.' WHERE `key` = 'hotel.navigator.search.maxresults'; +UPDATE `emulator_settings` SET `comment` = 'Respect order numbers when sorting navigator results.' WHERE `key` = 'hotel.navigator.sort.ordernum'; +UPDATE `emulator_settings` SET `comment` = 'Category ID used for the staff picks tab.' WHERE `key` = 'hotel.navigator.staffpicks.categoryid'; +UPDATE `emulator_settings` SET `comment` = 'Enable the NUX gift flow for new users.' WHERE `key` = 'hotel.nux.gifts.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of pets allowed in one inventory.' WHERE `key` = 'hotel.pets.max.inventory'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of pets allowed in one room.' WHERE `key` = 'hotel.pets.max.room'; +UPDATE `emulator_settings` SET `comment` = 'Maximum pet name length.' WHERE `key` = 'hotel.pets.name.length.max'; +UPDATE `emulator_settings` SET `comment` = 'Minimum pet name length.' WHERE `key` = 'hotel.pets.name.length.min'; +UPDATE `emulator_settings` SET `comment` = 'Generic player label used by text templates and client messages.' WHERE `key` = 'hotel.player.name'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of the same limited item a user can buy per day.' WHERE `key` = 'hotel.purchase.ltd.limit.daily.item'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of limited items a user can buy per day across all limited sales.' WHERE `key` = 'hotel.purchase.ltd.limit.daily.total'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in seconds before daily counters such as respect are refilled.' WHERE `key` = 'hotel.refill.daily'; +UPDATE `emulator_settings` SET `comment` = 'Maximum roller delay or speed value accepted by roller furniture.' WHERE `key` = 'hotel.rollers.speed.maximum'; +UPDATE `emulator_settings` SET `comment` = 'Enable room-entry logs.' WHERE `key` = 'hotel.room.enter.logs'; +UPDATE `emulator_settings` SET `comment` = 'Validate custom floorplans before rooms are saved.' WHERE `key` = 'hotel.room.floorplan.check.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum amount of furniture allowed in one room.' WHERE `key` = 'hotel.room.furni.max'; +UPDATE `emulator_settings` SET `comment` = 'Room ID used as the newbie lobby.' WHERE `key` = 'hotel.room.nooblobby'; +UPDATE `emulator_settings` SET `comment` = 'Kick users who stand on public room door tiles.' WHERE `key` = 'hotel.room.public.doortile.kick'; +UPDATE `emulator_settings` SET `comment` = 'Allow rollers to ignore normal placement rules.' WHERE `key` = 'hotel.room.rollers.norules'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of avatars that rollers can move at once.' WHERE `key` = 'hotel.room.rollers.roll_avatars.max'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of sticky notes allowed in one room.' WHERE `key` = 'hotel.room.stickies.max'; +UPDATE `emulator_settings` SET `comment` = 'Prefix template written by sticky pole furniture.' WHERE `key` = 'hotel.room.stickypole.prefix'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated staff room tags.' WHERE `key` = 'hotel.room.tags.staff'; +UPDATE `emulator_settings` SET `comment` = 'Allow empty rooms to switch into the idle state automatically.' WHERE `key` = 'hotel.rooms.auto.idle'; +UPDATE `emulator_settings` SET `comment` = 'Enable decoration-hosting features for rooms.' WHERE `key` = 'hotel.rooms.deco_hosting'; +UPDATE `emulator_settings` SET `comment` = 'Time in seconds before temporary hand items are cleared.' WHERE `key` = 'hotel.rooms.handitem.time'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of favorite rooms allowed per user.' WHERE `key` = 'hotel.rooms.max.favorite'; +UPDATE `emulator_settings` SET `comment` = 'Idle cycle count before a room user is marked idle.' WHERE `key` = 'hotel.roomuser.idle.cycles'; +UPDATE `emulator_settings` SET `comment` = 'Idle cycle count before a room user is kicked for idling.' WHERE `key` = 'hotel.roomuser.idle.cycles.kick'; +UPDATE `emulator_settings` SET `comment` = 'Ignore the wired idle status when checking the room idle rule.' WHERE `key` = 'hotel.roomuser.idle.not_dancing.ignore.wired_idle'; +UPDATE `emulator_settings` SET `comment` = 'Enable the sanctions system.' WHERE `key` = 'hotel.sanctions.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Modifier used by the shop discount calculation.' WHERE `key` = 'hotel.shop.discount.modifier'; +UPDATE `emulator_settings` SET `comment` = 'Enable the talent track feature.' WHERE `key` = 'hotel.talenttrack.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Offer ID requested when the client asks for a targeted offer.' WHERE `key` = 'hotel.targetoffer.id'; +UPDATE `emulator_settings` SET `comment` = 'Allow users to use teleports inside locked rooms when they otherwise qualify.' WHERE `key` = 'hotel.teleport.locked.allowed'; +UPDATE `emulator_settings` SET `comment` = 'Enable room trading.' WHERE `key` = 'hotel.trading.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Require the trading perk before users may trade.' WHERE `key` = 'hotel.trading.requires.perk'; +UPDATE `emulator_settings` SET `comment` = 'Maximum value used by `hotel.trophies.length.max`.' WHERE `key` = 'hotel.trophies.length.max'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onchangelooks.' WHERE `key` = 'hotel.users.clothingvalidation.onchangelooks'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onfballgate.' WHERE `key` = 'hotel.users.clothingvalidation.onfballgate'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onhcexpired.' WHERE `key` = 'hotel.users.clothingvalidation.onhcexpired'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onlogin.' WHERE `key` = 'hotel.users.clothingvalidation.onlogin'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onmannequin.' WHERE `key` = 'hotel.users.clothingvalidation.onmannequin'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onmimic.' WHERE `key` = 'hotel.users.clothingvalidation.onmimic'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of friends allowed for normal users.' WHERE `key` = 'hotel.users.max.friends'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of friends allowed for HC users.' WHERE `key` = 'hotel.users.max.friends.hc'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of rooms allowed for normal users.' WHERE `key` = 'hotel.users.max.rooms'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of rooms allowed for HC users.' WHERE `key` = 'hotel.users.max.rooms.hc'; +UPDATE `emulator_settings` SET `comment` = 'Enable the limited-countdown hotel-view widget.' WHERE `key` = 'hotel.view.ltdcountdown.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Item ID shown by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.itemid'; +UPDATE `emulator_settings` SET `comment` = 'Item name shown by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.itemname'; +UPDATE `emulator_settings` SET `comment` = 'Catalog page ID linked by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.pageid'; +UPDATE `emulator_settings` SET `comment` = 'Unix timestamp used by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.timestamp'; +UPDATE `emulator_settings` SET `comment` = 'Delay in milliseconds before the welcome alert is shown.' WHERE `key` = 'hotel.welcome.alert.delay'; +UPDATE `emulator_settings` SET `comment` = 'Enable the welcome alert shown after login.' WHERE `key` = 'hotel.welcome.alert.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Message template used by the welcome alert.' WHERE `key` = 'hotel.welcome.alert.message'; +UPDATE `emulator_settings` SET `comment` = 'Use the legacy welcome alert window style.' WHERE `key` = 'hotel.welcome.alert.oldstyle'; +UPDATE `emulator_settings` SET `comment` = 'Mute duration in minutes applied when word-filter automute is triggered.' WHERE `key` = 'hotel.wordfilter.automute'; +UPDATE `emulator_settings` SET `comment` = 'Enable the word filter system.' WHERE `key` = 'hotel.wordfilter.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Apply the word filter to messenger messages.' WHERE `key` = 'hotel.wordfilter.messenger'; +UPDATE `emulator_settings` SET `comment` = 'Normalise text before checking it against the word filter.' WHERE `key` = 'hotel.wordfilter.normalise'; +UPDATE `emulator_settings` SET `comment` = 'Replacement word used when text is censored.' WHERE `key` = 'hotel.wordfilter.replacement'; +UPDATE `emulator_settings` SET `comment` = 'Apply the word filter to room chat.' WHERE `key` = 'hotel.wordfilter.rooms'; +UPDATE `emulator_settings` SET `comment` = 'SQL query used to populate the hotel-view hall of fame panel.' WHERE `key` = 'hotelview.halloffame.query'; +UPDATE `emulator_settings` SET `comment` = 'Amount of activity points awarded by the hotel-view promotion.' WHERE `key` = 'hotelview.promotional.points'; +UPDATE `emulator_settings` SET `comment` = 'Activity point type used by the hotel-view promotional reward.' WHERE `key` = 'hotelview.promotional.points.type'; +UPDATE `emulator_settings` SET `comment` = 'Base item ID used by the hotel-view promotional reward.' WHERE `key` = 'hotelview.promotional.reward.id'; +UPDATE `emulator_settings` SET `comment` = 'Public item name used by the hotel-view promotional reward.' WHERE `key` = 'hotelview.promotional.reward.name'; +UPDATE `emulator_settings` SET `comment` = 'Generate images locally instead of relying on an external imager service.' WHERE `key` = 'imager.internal.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem path where badge part assets are stored.' WHERE `key` = 'imager.location.badgeparts'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem output path for generated badges.' WHERE `key` = 'imager.location.output.badges'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem output path for saved camera photos.' WHERE `key` = 'imager.location.output.camera'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem output path for generated camera thumbnails.' WHERE `key` = 'imager.location.output.thumbnail'; +UPDATE `emulator_settings` SET `comment` = 'Template URL used to fetch YouTube thumbnails.' WHERE `key` = 'imager.url.youtube'; +UPDATE `emulator_settings` SET `comment` = 'Client asset path used for the basejump gamecenter images.' WHERE `key` = 'images.gamecenter.basejump'; +UPDATE `emulator_settings` SET `comment` = 'Client asset path used for the snowwar gamecenter images.' WHERE `key` = 'images.gamecenter.snowwar'; +UPDATE `emulator_settings` SET `comment` = 'Show the hotel information panel or startup information message.' WHERE `key` = 'info.shown'; +UPDATE `emulator_settings` SET `comment` = 'Prevent invisible users from speaking in rooms.' WHERE `key` = 'invisible.prevent.chat'; +UPDATE `emulator_settings` SET `comment` = 'Number of Netty boss-group threads used by the socket server.' WHERE `key` = 'io.bossgroup.threads'; +UPDATE `emulator_settings` SET `comment` = 'Handle incoming client packets with a multi-threaded pipeline.' WHERE `key` = 'io.client.multithreaded.handler'; +UPDATE `emulator_settings` SET `comment` = 'Number of Netty worker-group threads used by the socket server.' WHERE `key` = 'io.workergroup.threads'; +UPDATE `emulator_settings` SET `comment` = 'Enable extra debug logging in the emulator logger.' WHERE `key` = 'logging.debug'; +UPDATE `emulator_settings` SET `comment` = 'Log packet parsing errors.' WHERE `key` = 'logging.errors.packets'; +UPDATE `emulator_settings` SET `comment` = 'Log runtime exceptions.' WHERE `key` = 'logging.errors.runtime'; +UPDATE `emulator_settings` SET `comment` = 'Log SQL errors.' WHERE `key` = 'logging.errors.sql'; +UPDATE `emulator_settings` SET `comment` = 'Log packet traffic in the standard logger.' WHERE `key` = 'logging.packets'; +UPDATE `emulator_settings` SET `comment` = 'Log undefined packets in the standard logger.' WHERE `key` = 'logging.packets.undefined'; +UPDATE `emulator_settings` SET `comment` = 'Global switch for the marketplace subsystem.' WHERE `key` = 'marketplace.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `monsterplant.seed.item_id`.' WHERE `key` = 'monsterplant.seed.item_id'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `monsterplant.seed_rare.item_id`.' WHERE `key` = 'monsterplant.seed_rare.item_id'; +UPDATE `emulator_settings` SET `comment` = 'Validate moodlight color values before applying them.' WHERE `key` = 'moodlight.color_check.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated navigator event category definitions shown in the events tab.' WHERE `key` = 'navigator.eventcategories'; +UPDATE `emulator_settings` SET `comment` = 'Enable TCP proxy-aware networking behaviour.' WHERE `key` = 'networking.tcp.proxy'; +UPDATE `emulator_settings` SET `comment` = 'Automatically notify staff when a chat report is created.' WHERE `key` = 'notify.staff.chat.auto.report'; +UPDATE `emulator_settings` SET `comment` = 'Base path used by the client to load furniture icon assets.' WHERE `key` = 'path.furniture.icons'; +UPDATE `emulator_settings` SET `comment` = 'Maximum pathfinder execution time in milliseconds before aborting.' WHERE `key` = 'pathfinder.execution_time.milli'; +UPDATE `emulator_settings` SET `comment` = 'Enforce the pathfinder execution time limit.' WHERE `key` = 'pathfinder.max_execution_time.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Allow the pathfinder to walk down falling steps.' WHERE `key` = 'pathfinder.step.allow.falling'; +UPDATE `emulator_settings` SET `comment` = 'Maximum height difference the pathfinder may step onto.' WHERE `key` = 'pathfinder.step.maximum.height'; +UPDATE `emulator_settings` SET `comment` = 'Chat bubble style ID used by the pirate parrot.' WHERE `key` = 'pirate_parrot.message.bubble'; +UPDATE `emulator_settings` SET `comment` = 'Number of predefined messages available to the pirate parrot.' WHERE `key` = 'pirate_parrot.message.count'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed on post-it notes.' WHERE `key` = 'postit.charlimit'; +UPDATE `emulator_settings` SET `comment` = 'Maximum delay allowed in the Pyramids minigame or puzzle timing.' WHERE `key` = 'pyramids.max.delay'; +UPDATE `emulator_settings` SET `comment` = 'Use retro-style home room behaviour in the navigator or onboarding flow.' WHERE `key` = 'retro.style.homeroom'; +UPDATE `emulator_settings` SET `comment` = 'Extra room chat delay applied before users can speak again.' WHERE `key` = 'room.chat.delay'; +UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper'; +UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format'; +UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge'; +UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url'; +UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats'; +UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell'; +UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types'; +UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement'; +UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end'; +UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire'; +UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency'; +UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval'; +UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date'; +UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak'; +UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval'; +UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1'; +UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey'; diff --git a/Database Updates/004_normalize_permissions_schema.sql b/Database Updates/004_normalize_permissions_schema.sql new file mode 100644 index 00000000..51582c5c --- /dev/null +++ b/Database Updates/004_normalize_permissions_schema.sql @@ -0,0 +1,499 @@ +-- Normalizes the legacy `permissions` table into: +-- 1. `permission_ranks` -> one row per rank with rank metadata. +-- 2. `permission_definitions` -> one row per permission key with comments and one `rank_` column per rank. +-- +-- This migration keeps the old `permissions` table untouched so the emulator can safely fall back to it. +-- It also cleans up the older experimental normalized objects if they were already created. + +DROP VIEW IF EXISTS `permissions_matrix_view`; +DROP PROCEDURE IF EXISTS `refresh_permissions_matrix_view`; +DROP TABLE IF EXISTS `permission_rank_values`; +DROP TABLE IF EXISTS `permission_nodes`; + +CREATE TABLE IF NOT EXISTS `permission_ranks` ( + `id` int(11) NOT NULL, + `rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `hidden_rank` tinyint(1) NOT NULL DEFAULT 0, + `badge` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `job_description` varchar(255) NOT NULL DEFAULT 'Here to help', + `staff_color` varchar(8) NOT NULL DEFAULT '#327fa8', + `staff_background` varchar(255) NOT NULL DEFAULT 'staff-bg.png', + `level` int(11) NOT NULL DEFAULT 1, + `room_effect` int(11) NOT NULL DEFAULT 0, + `log_commands` enum('0','1') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0', + `prefix` varchar(5) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `prefix_color` varchar(7) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `auto_credits_amount` int(11) DEFAULT 0, + `auto_pixels_amount` int(11) DEFAULT 0, + `auto_gotw_amount` int(11) DEFAULT 0, + `auto_points_amount` int(11) DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `permission_definitions` ( + `permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `max_value` tinyint(3) unsigned NOT NULL DEFAULT 1, + `comment` text NOT NULL, + PRIMARY KEY (`permission_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC; + +ALTER TABLE `permission_definitions` + DROP COLUMN IF EXISTS `category`, + DROP COLUMN IF EXISTS `value_type`, + DROP COLUMN IF EXISTS `sort_order`; + +INSERT INTO `permission_ranks` ( + `id`, + `rank_name`, + `hidden_rank`, + `badge`, + `job_description`, + `staff_color`, + `staff_background`, + `level`, + `room_effect`, + `log_commands`, + `prefix`, + `prefix_color`, + `auto_credits_amount`, + `auto_pixels_amount`, + `auto_gotw_amount`, + `auto_points_amount` +) +SELECT + `id`, + `rank_name`, + `hidden_rank`, + `badge`, + `job_description`, + `staff_color`, + `staff_background`, + `level`, + `room_effect`, + `log_commands`, + `prefix`, + `prefix_color`, + `auto_credits_amount`, + `auto_pixels_amount`, + `auto_gotw_amount`, + `auto_points_amount` +FROM `permissions` +ON DUPLICATE KEY UPDATE + `rank_name` = VALUES(`rank_name`), + `hidden_rank` = VALUES(`hidden_rank`), + `badge` = VALUES(`badge`), + `job_description` = VALUES(`job_description`), + `staff_color` = VALUES(`staff_color`), + `staff_background` = VALUES(`staff_background`), + `level` = VALUES(`level`), + `room_effect` = VALUES(`room_effect`), + `log_commands` = VALUES(`log_commands`), + `prefix` = VALUES(`prefix`), + `prefix_color` = VALUES(`prefix_color`), + `auto_credits_amount` = VALUES(`auto_credits_amount`), + `auto_pixels_amount` = VALUES(`auto_pixels_amount`), + `auto_gotw_amount` = VALUES(`auto_gotw_amount`), + `auto_points_amount` = VALUES(`auto_points_amount`); + +DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`; + +DELIMITER $$ +CREATE PROCEDURE `refresh_permission_definition_rank_columns`() +BEGIN + DECLARE done INT DEFAULT 0; + DECLARE current_rank_id INT; + DECLARE current_column_name VARCHAR(32); + DECLARE column_exists INT DEFAULT 0; + DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; + + OPEN rank_cursor; + + rank_loop: LOOP + FETCH rank_cursor INTO current_rank_id; + + IF done = 1 THEN + LEAVE rank_loop; + END IF; + + SET current_column_name = CONCAT('rank_', current_rank_id); + + SELECT COUNT(*) + INTO column_exists + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = 'permission_definitions' + AND `column_name` = current_column_name; + + IF column_exists = 0 THEN + SET @alter_permissions_column_sql = CONCAT( + 'ALTER TABLE `permission_definitions` ADD COLUMN `', + current_column_name, + '` tinyint(3) unsigned NOT NULL DEFAULT 0' + ); + + PREPARE alter_permissions_column_stmt FROM @alter_permissions_column_sql; + EXECUTE alter_permissions_column_stmt; + DEALLOCATE PREPARE alter_permissions_column_stmt; + END IF; + END LOOP; + + CLOSE rank_cursor; +END$$ +DELIMITER ; + +CALL `refresh_permission_definition_rank_columns`(); + +INSERT INTO `permission_definitions` ( + `permission_key`, + `max_value`, + `comment` +) +SELECT + `column_name` AS `permission_key`, + CASE + WHEN `column_type` LIKE '%''2''%' THEN 2 + ELSE 1 + END AS `max_value`, + CASE + WHEN COALESCE(`column_comment`, '') <> '' THEN `column_comment` + WHEN `column_name` LIKE 'cmd\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT( + 'Controls access to the :', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' command. Values: 0 = disabled, 1 = allowed, 2 = allowed only when room-owner rights may be used.' + ) + WHEN `column_name` LIKE 'cmd\_%' THEN CONCAT( + 'Controls access to the :', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' command. Values: 0 = disabled, 1 = allowed.' + ) + WHEN `column_name` LIKE 'acc\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT( + 'Controls the ', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' capability for this rank. Values: 0 = disabled, 1 = enabled, 2 = enabled only when room-owner rights may be used.' + ) + WHEN `column_name` LIKE 'acc\_%' THEN CONCAT( + 'Controls the ', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' capability for this rank. Values: 0 = disabled, 1 = enabled.' + ) + ELSE CONCAT( + 'Legacy permission-related value migrated from the old permissions table for ', + `column_name`, + '.' + ) + END AS `comment` +FROM `information_schema`.`columns` +WHERE `table_schema` = DATABASE() + AND `table_name` = 'permissions' + AND `column_name` NOT IN ( + 'id', + 'rank_name', + 'hidden_rank', + 'badge', + 'job_description', + 'staff_color', + 'staff_background', + 'level', + 'room_effect', + 'log_commands', + 'prefix', + 'prefix_color', + 'auto_credits_amount', + 'auto_pixels_amount', + 'auto_gotw_amount', + 'auto_points_amount' + ) +ON DUPLICATE KEY UPDATE + `max_value` = VALUES(`max_value`), + `comment` = VALUES(`comment`); + +DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`; + +CREATE TEMPORARY TABLE `tmp_permission_comments` ( + `permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`permission_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci; + +INSERT INTO `tmp_permission_comments` (`permission_key`, `comment`) VALUES +('cmd_about', 'Allows using :about to display emulator, revision, or hotel information exposed by the command.'), +('cmd_alert', 'Allows using :alert to send a hotel alert popup to a specific user.'), +('cmd_allow_trading', 'Allows using the trading-toggle command to enable or disable trading for a target user.'), +('cmd_badge', 'Allows granting a badge code to a target user through a command.'), +('cmd_ban', 'Allows banning users from the hotel.'), +('cmd_blockalert', 'Allows sending the block-alert style moderation message.'), +('cmd_bots', 'Allows using :bots to list the bots currently placed in the room.'), +('cmd_bundle', 'Allows using :bundle / :roombundle to create a catalog room-bundle offer for the current room.'), +('cmd_calendar', 'Allows using the hotel calendar command and any calendar actions wired to that command entry.'), +('cmd_changename', 'Allows forcing a user-name change through the change-name command flow.'), +('cmd_chatcolor', 'Allows changing the active chat bubble color through the chat-color command.'), +('cmd_commands', 'Allows using :commands to list the command keys available to the current user.'), +('cmd_connect_camera', 'Allows using the command that links the in-room camera feature to the current room session.'), +('cmd_control', 'Allows using :control to take over another in-room user and stop controlling them later.'), +('cmd_coords', 'Allows using :coords to inspect room coordinates for tiles, users, or furniture.'), +('cmd_credits', 'Allows giving or removing credits from a user through the staff currency command.'), +('cmd_subscription', 'Allows granting or editing subscription time through the subscription command.'), +('cmd_danceall', 'Allows forcing every Habbo currently in the room to dance.'), +('cmd_diagonal', 'Allows toggling diagonal walking for the current room.'), +('cmd_disconnect', 'Allows disconnecting a user from the hotel immediately.'), +('cmd_duckets', 'Allows giving or removing duckets from a user through the staff currency command.'), +('cmd_ejectall', 'Allows ejecting all users from the current room.'), +('cmd_empty', 'Allows clearing the current user furniture inventory through the empty-inventory command.'), +('cmd_empty_bots', 'Allows clearing the current user bot inventory through the empty-bots command.'), +('cmd_empty_pets', 'Allows clearing the current user pet inventory through the empty-pets command.'), +('cmd_enable', 'Allows applying an avatar effect to yourself, or to another user when acc_enable_others is also granted.'), +('cmd_event', 'Allows marking the current room as an event room through the event command.'), +('cmd_faceless', 'Allows toggling the faceless avatar visual state on the executing room unit.'), +('cmd_fastwalk', 'Allows toggling fast-walk mode for yourself or another in-room user.'), +('cmd_filterword', 'Allows adding or removing entries from the configured word filter through command usage.'), +('cmd_freeze', 'Allows freezing a target user in place.'), +('cmd_freeze_bots', 'Allows freezing bots that are placed in the room.'), +('cmd_gift', 'Allows sending a gift to a target user through the gift command.'), +('cmd_give_rank', 'Allows setting another user rank through the give-rank command.'), +('cmd_ha', 'Allows sending a hotel-wide alert.'), +('acc_can_stalk', 'Allows following users even when they have disabled stalking.'), +('cmd_hal', 'Allows sending a hotel-wide alert with a clickable link or extended content.'), +('cmd_invisible', 'Allows toggling invisible staff mode.'), +('cmd_ip_ban', 'Allows banning a user by IP address.'), +('cmd_machine_ban', 'Allows banning a user by machine identifier.'), +('cmd_hand_item', 'Allows spawning or changing the hand item currently held by a user.'), +('cmd_happyhour', 'Allows starting or stopping the happy-hour event flow exposed by the happyhour command.'), +('cmd_hidewired', 'Allows toggling whether wired furniture is visually hidden in the current room.'), +('cmd_kickall', 'Allows kicking every user from the current room.'), +('cmd_softkick', 'Allows soft-kicking a user back to the hotel view without a full sanction.'), +('cmd_massbadge', 'Allows giving the same badge to many users at once.'), +('cmd_roombadge', 'Allows setting or overriding the room badge shown to users.'), +('cmd_masscredits', 'Allows giving credits to many users at once through the mass-credits command.'), +('cmd_massduckets', 'Allows giving duckets to many users at once through the mass-duckets command.'), +('cmd_massgift', 'Allows sending the same gift to many users at once.'), +('cmd_masspoints', 'Allows giving activity points to many users at once through the mass-points command.'), +('cmd_moonwalk', 'Allows toggling the moonwalk avatar effect for yourself while you are inside a room.'), +('cmd_mimic', 'Allows copying another user appearance or presence state through the mimic command.'), +('cmd_multi', 'Allows executing multiple chat commands from the special sticky/post-it scripting payload.'), +('cmd_mute', 'Allows muting a target user.'), +('cmd_pet_info', 'Allows opening the detailed pet-information view for a pet.'), +('cmd_pickall', 'Allows picking up every furniture item from the current room.'), +('cmd_plugins', 'Legacy key for the :plugins command, which currently lists loaded plugins without enforcing this dedicated permission node in code.'), +('cmd_points', 'Allows giving or removing activity points from a user through the points command.'), +('cmd_promote_offer', 'Allows using :promoteoffer to list active target offers or switch the globally promoted target offer.'), +('cmd_pull', 'Allows pulling a nearby user onto the tile directly in front of you.'), +('cmd_push', 'Allows pushing the user standing in front of you one tile farther in the direction you are facing.'), +('cmd_redeem', 'Allows redeeming redeemable inventory items through the redeem command flow.'), +('cmd_reload_room', 'Allows unloading and reloading the current room, then forwarding the occupants back into the fresh room instance.'), +('cmd_roomalert', 'Allows sending the same alert message to everyone in the current room.'), +('cmd_roomcredits', 'Allows giving credits to every Habbo currently in the room.'), +('cmd_roomeffect', 'Allows applying the same avatar effect id to every Habbo currently in the room.'), +('cmd_roomgift', 'Allows sending the same gift to every Habbo currently in the room.'), +('cmd_roomitem', 'Allows setting the same hand-item id for every Habbo in the room; using 0 clears the hand item.'), +('cmd_roommute', 'Allows muting every Habbo currently in the room.'), +('cmd_roompixels', 'Allows giving duckets or pixels to every Habbo currently in the room.'), +('cmd_roompoints', 'Allows giving activity points to every Habbo currently in the room.'), +('cmd_say', 'Allows forcing another online user to say a custom message in their current room.'), +('cmd_say_all', 'Allows making everyone in the room say a message.'), +('cmd_setmax', 'Allows using :setmax to change the maximum user capacity of the current room.'), +('cmd_set_poll', 'Allows using :setpoll to attach or remove a poll on the current room.'), +('cmd_setpublic', 'Allows using :setpublic to change the room public/private visibility state.'), +('cmd_setspeed', 'Allows using :setspeed to change the room walking speed setting.'), +('cmd_shout', 'Allows forcing another online user to shout a custom message in their current room.'), +('cmd_shout_all', 'Allows making everyone in the room shout a message.'), +('cmd_shutdown', 'Allows using the shutdown command to stop the emulator process.'), +('cmd_sitdown', 'Allows forcing users to sit down through the sitdown command.'), +('cmd_staffalert', 'Allows sending an alert that is visible only to staff members.'), +('cmd_staffonline', 'Allows viewing the current list of online staff members.'), +('cmd_summon', 'Allows summoning a target user into the room where the staff member currently is.'), +('cmd_summonrank', 'Allows summoning all online users of a given rank into the current room.'), +('cmd_super_ban', 'Allows issuing the strongest ban command variant exposed by the super-ban command.'), +('cmd_stalk', 'Allows following another user to their room.'), +('cmd_superpull', 'Allows pulling a user to the tile in front of you without the short-range reach check used by :pull.'), +('cmd_take_badge', 'Allows removing a badge code from a target user.'), +('cmd_talk', 'Allows using the legacy :talk command to make another user speak a command-provided message.'), +('cmd_teleport', 'Allows toggling the room-unit teleport mode used by the :teleport command.'), +('cmd_trash', 'Allows deleting or trashing furniture/items through the trash command flow.'), +('cmd_transform', 'Allows transforming your room unit into a chosen pet type, race, and color.'), +('cmd_unban', 'Allows removing active bans.'), +('cmd_unload', 'Allows disposing the current room instance immediately through :unload / :crash.'), +('cmd_unmute', 'Allows removing an active mute from a target user.'), +('cmd_update_achievements', 'Allows using :update_achievements to reload achievements configuration.'), +('cmd_update_bots', 'Allows using :update_bots to reload bot data and bot configuration.'), +('cmd_update_catalogue', 'Allows using :update_catalogue to reload catalogue pages and offers.'), +('cmd_update_config', 'Allows using :update_config to reload emulator configuration settings.'), +('cmd_update_guildparts', 'Allows using :update_guildparts to reload guild badge parts and guild configuration.'), +('cmd_update_hotel_view', 'Allows using :update_hotel_view to reload hotel-view assets or settings.'), +('cmd_update_items', 'Allows using :update_items to reload item data and furniture definitions.'), +('cmd_update_navigator', 'Allows using :update_navigator to reload navigator configuration and listings.'), +('cmd_update_permissions', 'Allows using :update_permissions to reload ranks and permissions from the database.'), +('cmd_update_pet_data', 'Allows using :update_pet_data to reload pet types and pet races.'), +('cmd_update_plugins', 'Allows using :update_plugins to reload plugin data or plugin metadata.'), +('cmd_update_polls', 'Allows using :update_polls to reload poll and questionnaire data.'), +('cmd_update_texts', 'Allows using :update_texts to reload external texts and localizations.'), +('cmd_update_wordfilter', 'Allows using :update_wordfilter to reload the word-filter list.'), +('cmd_userinfo', 'Allows opening the detailed user-information view used by staff tools.'), +('cmd_word_quiz', 'Allows starting a room word-quiz event with a custom question and optional duration.'), +('cmd_warp', 'Allows instantly warping your room unit to a target tile.'), +('acc_anychatcolor', 'Allows selecting any chat bubble color, including normally restricted colors.'), +('acc_anyroomowner', 'Treats the rank as room owner for owner-only checks such as room settings, wired saving, rights management, floorplan editing, and similar room-owner gates.'), +('acc_empty_others', 'Allows :empty, :empty_bots, and :empty_pets to target another user inventory instead of only your own.'), +('acc_enable_others', 'Allows :enable to apply avatar effects to another user instead of only to yourself.'), +('acc_see_whispers', 'Allows seeing whispers sent between other users in the room.'), +('acc_see_tentchat', 'Allows seeing tent chat or similar hidden chat channels that are normally not visible to everyone.'), +('acc_superwired', 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.'), +('acc_supporttool', 'Allows opening and using the support/moderation tool interface.'), +('acc_unkickable', 'Prevents the user from being kicked by normal moderation or room commands.'), +('acc_guildgate', 'Allows bypassing guild gate access restrictions.'), +('acc_moverotate', 'Allows moving, rotating, and saving wired furniture without the usual room-owner restriction checks.'), +('acc_placefurni', 'Allows placing furniture, opening :wired, and passing room-right checks that normally require owner or controller rights.'), +('acc_unlimited_bots', 'Removes both the bot inventory cap and the per-room bot placement cap for this rank.'), +('acc_unlimited_pets', 'Removes both the pet inventory cap and the per-room pet placement cap for this rank.'), +('acc_hide_ip', 'Hides the user IP address in staff tools and other staff-facing views.'), +('acc_hide_mail', 'Hides the user email address in moderation tools and staff views.'), +('acc_not_mimiced', 'Prevents other users from mimicking this account.'), +('acc_chat_no_flood', 'Exempts the user from flood protection limits.'), +('acc_staff_chat', 'Allows accessing staff-only chat channels and staff broadcasts.'), +('acc_staff_pick', 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.'), +('acc_enteranyroom', 'Allows entering rooms regardless of door mode, bans, or normal access restrictions.'), +('acc_fullrooms', 'Allows entering rooms even when they are at maximum user capacity.'), +('acc_infinite_credits', 'Prevents credits from being consumed when a command or purchase checks credit balance.'), +('acc_infinite_pixels', 'Prevents duckets or pixels from being consumed when the balance is checked.'), +('acc_infinite_points', 'Prevents activity points from being consumed when the balance is checked.'), +('acc_ambassador', 'Marks the rank as an ambassador for ambassador-only tools and visuals.'), +('acc_debug', 'Allows using debug-only features, commands, or internal tooling.'), +('acc_chat_no_limit', 'Lets the user hear and be heard regardless of room hearing distance limits.'), +('acc_chat_no_filter', 'Bypasses the word filter for chat and staff-generated messages.'), +('acc_nomute', 'Prevents the user from being muted by normal mute checks.'), +('acc_guild_admin', 'Allows bypassing guild admin restrictions when managing guilds.'), +('acc_catalog_ids', 'Allows seeing internal catalogue page ids, offer ids, or related technical catalogue identifiers.'), +('acc_modtool_ticket_q', 'Allows seeing and handling the moderation ticket queue.'), +('acc_modtool_user_logs', 'Allows reading user chat logs in the moderation tool.'), +('acc_modtool_user_alert', 'Allows sending moderation alerts or cautions to users.'), +('acc_modtool_user_kick', 'Allows kicking users from the moderation tool.'), +('acc_modtool_user_ban', 'Allows banning users from the moderation tool.'), +('acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'), +('acc_modtool_room_logs', 'Allows viewing room chat logs in the moderation tool.'), +('acc_trade_anywhere', 'Allows starting trades outside the normal trade-enabled areas.'), +('acc_update_notifications', 'Allows receiving update notifications emitted by the emulator.'), +('acc_helper_use_guide_tool', 'Allows opening the helper guide tool.'), +('acc_helper_give_guide_tours', 'Allows accepting and handling guide tour requests.'), +('acc_helper_judge_chat_reviews', 'Allows reviewing helper or chat review tickets.'), +('acc_floorplan_editor', 'Allows opening and saving the floorplan editor.'), +('acc_camera', 'Allows using the in-room camera feature and related camera UI actions.'), +('acc_ads_background', 'Allows editing room advertisement backgrounds.'), +('cmd_wordquiz', 'Legacy alias of cmd_word_quiz for starting a room word-quiz event.'), +('acc_room_staff_tags', 'Shows staff tags or markers above the user while inside rooms.'), +('acc_infinite_friends', 'Removes the normal friend-list size limit.'), +('acc_mimic_unredeemed', 'Allows mimicking looks even when they contain unreleased or restricted clothing.'), +('cmd_update_youtube_playlists', 'Allows reloading YouTube playlist configuration for furniture integrations.'), +('cmd_add_youtube_playlist', 'Allows adding a new YouTube playlist entry.'), +('acc_mention', 'Allows using mention-related chat features beyond the normal rank restriction.'), +('cmd_setstate', 'Legacy room-editor permission for :setstate / :ss, used to change the selected furni state or extradata value.'), +('cmd_buildheight', 'Legacy room-editor permission for :buildheight / :bh, used to change the room build-height override.'), +('cmd_setrotation', 'Legacy room-editor permission for :setrotation / :rot, used to change the rotation of the selected furni.'), +('cmd_sellroom', 'Allows putting the current room up for sale through the sell-room command.'), +('cmd_buyroom', 'Allows purchasing a room that is marked as for sale through the buy-room command.'), +('cmd_pay', 'Allows transferring currency to another user through the pay command.'), +('cmd_kill', 'Allows using the kill command effect exposed by the current command set.'), +('cmd_hoverboard', 'Allows toggling the hoverboard effect or hoverboard movement mode.'), +('cmd_kiss', 'Allows using the kiss interaction command on another user.'), +('cmd_hug', 'Allows using the hug interaction command on another user.'), +('cmd_welcome', 'Allows triggering the welcome command behavior defined by the current command set.'), +('cmd_disable_effects', 'Allows disabling active avatar effects through the disable-effects command.'), +('cmd_brb', 'Allows toggling the be-right-back status command.'), +('cmd_nuke', 'Allows using the nuke command exposed by the current command set.'), +('cmd_slime', 'Allows applying the slime command/effect exposed by the current command set.'), +('cmd_explain', 'Allows using the explain command to send the predefined explanation/help flow to users.'), +('cmd_closedice', 'Legacy essentials permission for :closedice, used to close dice items in the room or all dice at once.'), +('acc_closedice_room', 'Legacy companion permission used by older closed-dice room checks.'), +('cmd_set', 'Legacy essentials permission for :set / :changefurni, the generic furni editing command documented by :set info.'), +('cmd_furnidata', 'Allows viewing technical furnidata information in-game for selected furniture.'), +('kiss_cmd', 'Legacy alias used for the kiss command permission.'), +('acc_calendar_force', 'Allows claiming calendar rewards even when the normal day-difference timing check would block the claim.'), +('cmd_update_calendar', 'Allows using :update_calendar to reload calendar definitions and rewards.'), +('cmd_update_all', 'Allows using :update_all to reload all supported runtime data sets in one command.'), +('cms_dance', 'Legacy CMS-side permission kept for website integrations; no direct in-emulator command handler was found in the current tree.'), +('acc_catalogfurni', 'Allows using catalogue administration features related to furniture pages and offers.'), +('acc_unignorable', 'Prevents the account from being ignored by other users through the ignore system.'), +('cmd_update_chat_bubbles', 'Allows using :update_chat_bubbles to reload chat-bubble definitions and assets.'), +('cmd_calendar_staff', 'Allows the staff-only actions exposed by the calendar command flow.'); + +UPDATE `permission_definitions` pd +INNER JOIN `tmp_permission_comments` tc ON tc.`permission_key` = pd.`permission_key` +SET pd.`comment` = tc.`comment`; + +DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`; + +DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`; + +DELIMITER $$ +CREATE PROCEDURE `refresh_permission_definition_values`() +BEGIN + DECLARE done INT DEFAULT 0; + DECLARE current_rank_id INT; + DECLARE current_column_name VARCHAR(32); + DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; + + OPEN rank_cursor; + + rank_loop: LOOP + FETCH rank_cursor INTO current_rank_id; + + IF done = 1 THEN + LEAVE rank_loop; + END IF; + + SET current_column_name = CONCAT('rank_', current_rank_id); + + SELECT GROUP_CONCAT( + CONCAT( + 'SELECT ''', + REPLACE(`column_name`, '''', ''''''), + ''' AS permission_key, CAST(COALESCE(`', + REPLACE(`column_name`, '`', '``'), + '`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions` WHERE `id` = ', + current_rank_id + ) + ORDER BY `ordinal_position` + SEPARATOR ' UNION ALL ' + ) INTO @permission_rank_source_sql + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = 'permissions' + AND `column_name` NOT IN ( + 'id', + 'rank_name', + 'hidden_rank', + 'badge', + 'job_description', + 'staff_color', + 'staff_background', + 'level', + 'room_effect', + 'log_commands', + 'prefix', + 'prefix_color', + 'auto_credits_amount', + 'auto_pixels_amount', + 'auto_gotw_amount', + 'auto_points_amount' + ); + + SET @permission_rank_update_sql = CONCAT( + 'UPDATE `permission_definitions` pd ', + 'INNER JOIN (', + @permission_rank_source_sql, + ') src ON src.permission_key = pd.permission_key ', + 'SET pd.`', + current_column_name, + '` = src.permission_value' + ); + + PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql; + EXECUTE permission_rank_update_stmt; + DEALLOCATE PREPARE permission_rank_update_stmt; + END LOOP; + + CLOSE rank_cursor; +END$$ +DELIMITER ; + +CALL `refresh_permission_definition_values`(); diff --git a/Database Updates/005_add_room_wired_settings.sql b/Database Updates/005_add_room_wired_settings.sql new file mode 100644 index 00000000..82c8b8e7 --- /dev/null +++ b/Database Updates/005_add_room_wired_settings.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `room_wired_settings` ( + `room_id` int(11) NOT NULL, + `inspect_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.', + `modify_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.', + PRIMARY KEY (`room_id`), + CONSTRAINT `fk_room_wired_settings_room_id` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/Database Updates/006_add_room_user_wired_variables.sql b/Database Updates/006_add_room_user_wired_variables.sql new file mode 100644 index 00000000..706ea78e --- /dev/null +++ b/Database Updates/006_add_room_user_wired_variables.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS `room_user_wired_variables` ( + `room_id` int(11) NOT NULL, + `user_id` int(11) NOT NULL, + `variable_item_id` int(11) NOT NULL, + `value` int(11) DEFAULT NULL, + `created_at` int(11) NOT NULL DEFAULT 0, + `updated_at` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`), + KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`), + KEY `idx_room_user_wired_variables_user` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` ( + `room_id` int(11) NOT NULL, + `furni_id` int(11) NOT NULL, + `variable_item_id` int(11) NOT NULL, + `value` int(11) DEFAULT NULL, + `created_at` int(11) NOT NULL DEFAULT 0, + `updated_at` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`), + KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`), + KEY `idx_room_furni_wired_variables_furni` (`furni_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `room_wired_variables` ( + `room_id` int(11) NOT NULL, + `variable_item_id` int(11) NOT NULL, + `value` int(11) NOT NULL DEFAULT 0, + `created_at` int(11) NOT NULL DEFAULT 0, + `updated_at` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`room_id`, `variable_item_id`), + KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/Database Updates/007_add_wired_variable_timestamps.sql b/Database Updates/007_add_wired_variable_timestamps.sql new file mode 100644 index 00000000..313531a8 --- /dev/null +++ b/Database Updates/007_add_wired_variable_timestamps.sql @@ -0,0 +1,32 @@ +ALTER TABLE `room_user_wired_variables` + ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`; + +ALTER TABLE `room_user_wired_variables` + ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`; + +UPDATE `room_user_wired_variables` +SET + `created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()), + `updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP())); + +ALTER TABLE `room_furni_wired_variables` + ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`; + +ALTER TABLE `room_furni_wired_variables` + ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`; + +UPDATE `room_furni_wired_variables` +SET + `created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()), + `updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP())); + +ALTER TABLE `room_wired_variables` + ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`; + +ALTER TABLE `room_wired_variables` + ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`; + +UPDATE `room_wired_variables` +SET + `created_at` = 0, + `updated_at` = IF(`updated_at` > 0, `updated_at`, UNIX_TIMESTAMP()); diff --git a/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java b/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java index 2e4819a4..1b0065b2 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java +++ b/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java @@ -17,14 +17,18 @@ import java.util.Properties; public class ConfigurationManager { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationManager.class); + private static final String EMULATOR_SETTINGS_TABLE = "emulator_settings"; + private static final String WIRED_SETTINGS_TABLE = "wired_emulator_settings"; private final Properties properties; + private final Properties wiredProperties; private final String configurationPath; public boolean loaded = false; public boolean isLoading = false; public ConfigurationManager(String configurationPath) { this.properties = new Properties(); + this.wiredProperties = new Properties(); this.configurationPath = configurationPath; this.reload(); } @@ -32,6 +36,7 @@ public class ConfigurationManager { public void reload() { this.isLoading = true; this.properties.clear(); + this.wiredProperties.clear(); InputStream input = null; @@ -116,31 +121,15 @@ public class ConfigurationManager { LOGGER.info("Loading configuration from database..."); long millis = System.currentTimeMillis(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement()) { - if (statement.execute("SELECT * FROM emulator_settings")) { - try (ResultSet set = statement.getResultSet()) { - while (set.next()) { - this.properties.put(set.getString("key"), set.getString("value")); - } - } - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + this.loadSettingsTable(EMULATOR_SETTINGS_TABLE, this.properties, false); + this.loadSettingsTable(WIRED_SETTINGS_TABLE, this.wiredProperties, true); LOGGER.info("Configuration -> loaded! ({} MS)", System.currentTimeMillis() - millis); } public void saveToDatabase() { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE emulator_settings SET `value` = ? WHERE `key` = ? LIMIT 1")) { - for (Map.Entry entry : this.properties.entrySet()) { - statement.setString(1, entry.getValue().toString()); - statement.setString(2, entry.getKey().toString()); - statement.executeUpdate(); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + this.saveSettingsTable(EMULATOR_SETTINGS_TABLE, this.properties); + this.saveSettingsTable(WIRED_SETTINGS_TABLE, this.wiredProperties); } @@ -153,10 +142,21 @@ public class ConfigurationManager { if (this.isLoading) return defaultValue; - if (!this.properties.containsKey(key)) { + Properties targetProperties = this.resolveProperties(key); + + if (targetProperties.containsKey(key)) { + return targetProperties.getProperty(key, defaultValue); + } + + if (this.isWiredSettingKey(key) && this.properties.containsKey(key)) { + return this.properties.getProperty(key, defaultValue); + } + + if (!targetProperties.containsKey(key)) { LOGGER.error("Config key not found {}", key); } - return this.properties.getProperty(key, defaultValue); + + return defaultValue; } public boolean getBoolean(String key) { @@ -209,21 +209,91 @@ public class ConfigurationManager { } public void update(String key, String value) { - this.properties.setProperty(key, value); + this.resolveProperties(key).setProperty(key, value); } public void register(String key, String value) { - if (this.properties.getProperty(key, null) != null) + this.register(key, value, ""); + } + + public void register(String key, String value, String comment) { + Properties targetProperties = this.resolveProperties(key); + + if (targetProperties.getProperty(key, null) != null) return; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO emulator_settings VALUES (?, ?)")) { - statement.setString(1, key); - statement.setString(2, value); - statement.execute(); - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - + this.insertSetting(key, value, comment); this.update(key, value); } + + private void loadSettingsTable(String tableName, Properties targetProperties, boolean optional) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + Statement statement = connection.createStatement()) { + if (statement.execute("SELECT * FROM " + tableName)) { + try (ResultSet set = statement.getResultSet()) { + while (set.next()) { + targetProperties.put(set.getString("key"), set.getString("value")); + } + } + } + } catch (SQLException e) { + if (optional) { + LOGGER.warn("Skipping optional config table {}: {}", tableName, e.getMessage()); + } else { + LOGGER.error("Caught SQL exception", e); + } + } + } + + private void saveSettingsTable(String tableName, Properties sourceProperties) { + String sql = "UPDATE " + tableName + " SET `value` = ? WHERE `key` = ? LIMIT 1"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + for (Map.Entry entry : sourceProperties.entrySet()) { + statement.setString(1, entry.getValue().toString()); + statement.setString(2, entry.getKey().toString()); + statement.executeUpdate(); + } + } catch (SQLException e) { + if (WIRED_SETTINGS_TABLE.equals(tableName)) { + LOGGER.warn("Skipping wired config save for table {}: {}", tableName, e.getMessage()); + } else { + LOGGER.error("Caught SQL exception", e); + } + } + } + + private void insertSetting(String key, String value, String comment) { + String tableName = this.isWiredSettingKey(key) ? WIRED_SETTINGS_TABLE : EMULATOR_SETTINGS_TABLE; + String sql = this.isWiredSettingKey(key) + ? "INSERT INTO " + tableName + " (`key`, `value`, `comment`) VALUES (?, ?, ?)" + : "INSERT INTO " + tableName + " (`key`, `value`) VALUES (?, ?)"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, key); + statement.setString(2, value); + + if (this.isWiredSettingKey(key)) { + statement.setString(3, comment == null ? "" : comment); + } + + statement.execute(); + } catch (SQLException e) { + if (this.isWiredSettingKey(key)) { + LOGGER.warn("Unable to insert wired setting {} into {}: {}", key, tableName, e.getMessage()); + } else { + LOGGER.error("Caught SQL exception", e); + } + } + } + + private Properties resolveProperties(String key) { + return this.isWiredSettingKey(key) ? this.wiredProperties : this.properties; + } + + private boolean isWiredSettingKey(String key) { + return key != null && (key.startsWith("wired.") || key.startsWith("hotel.wired.")); + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/WiredCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/WiredCommand.java index d8d869b9..59ab1a4b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/WiredCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/WiredCommand.java @@ -1,14 +1,13 @@ package com.eu.habbo.habbohotel.commands; import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; import com.eu.habbo.messages.outgoing.users.InClientLinkComposer; public class WiredCommand extends Command { public WiredCommand() { - super(Permission.ACC_PLACEFURNI, new String[]{"wired"}); + super(null, new String[]{"wired"}); } @Override @@ -20,12 +19,8 @@ public class WiredCommand extends Command { return true; } - boolean hasRights = room.hasRights(gameClient.getHabbo()) - || room.isOwner(gameClient.getHabbo()) - || gameClient.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER); - - if (!hasRights) { - gameClient.getHabbo().whisper("You need room rights to open the Wired Creator Tools.", RoomChatMessageBubbles.ALERT); + if (!room.canInspectWired(gameClient.getHabbo())) { + gameClient.sendResponse(new InClientLinkComposer("wired-tools/invalid")); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java index 796f3f0c..411a530b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java @@ -52,7 +52,10 @@ import com.eu.habbo.habbohotel.items.interactions.wired.effects.*; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraAnimationTime; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveCarryUsers; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; @@ -60,9 +63,18 @@ import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMovePhys import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveNoAnimation; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputFurniName; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextInputVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputUsername; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableLevelUpSystem; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector; import com.eu.habbo.habbohotel.items.interactions.wired.selector.*; import com.eu.habbo.habbohotel.items.interactions.wired.triggers.*; import com.eu.habbo.habbohotel.users.Habbo; @@ -225,6 +237,7 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_trg_leave_room", WiredTriggerHabboLeavesRoom.class)); this.interactionsList.add(new ItemInteraction("wf_trg_says_something", WiredTriggerHabboSaysKeyword.class)); this.interactionsList.add(new ItemInteraction("wf_trg_clock_counter", WiredTriggerClockCounter.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_var_changed", WiredTriggerVariableChanged.class)); this.interactionsList.add(new ItemInteraction("wf_trg_periodically", WiredTriggerRepeater.class)); this.interactionsList.add(new ItemInteraction("wf_trg_period_short", WiredTriggerRepeaterShort.class)); this.interactionsList.add(new ItemInteraction("wf_trg_period_long", WiredTriggerRepeaterLong.class)); @@ -300,7 +313,12 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_slc_users_handitem", WiredEffectUsersHandItem.class)); this.interactionsList.add(new ItemInteraction("wf_slc_users_onfurni", WiredEffectUsersOnFurni.class)); this.interactionsList.add(new ItemInteraction("wf_slc_users_group", WiredEffectUsersGroup.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_furni_with_var", WiredEffectFurniWithVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_with_var", WiredEffectUsersWithVariable.class)); this.interactionsList.add(new ItemInteraction("wf_act_send_signal", WiredEffectSendSignal.class)); + this.interactionsList.add(new ItemInteraction("wf_act_give_var", WiredEffectGiveVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_act_remove_var", WiredEffectRemoveVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_act_change_var_val", WiredEffectChangeVariableValue.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_has_furni_on", WiredConditionFurniHaveFurni.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_furnis_hv_avtrs", WiredConditionFurniHaveHabbo.class)); @@ -340,6 +358,10 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_cnd_not_triggerer_match", WiredConditionNotTriggererMatch.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_team_has_score", WiredConditionTeamHasScore.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_team_has_rank", WiredConditionTeamHasRank.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_has_var", WiredConditionHasVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_neg_has_var", WiredConditionNotHasVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_var_val_match", WiredConditionVariableValueMatch.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_var_age_match", WiredConditionVariableAgeMatch.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_random", WiredExtraRandom.class)); @@ -349,6 +371,8 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_xtra_filter_furni", WiredExtraFilterFurni.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_filter_user", WiredExtraFilterUser.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_filter_users", WiredExtraFilterUser.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_filter_furni_by_var", WiredExtraFilterFurniByVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_filter_users_by_var", WiredExtraFilterUsersByVariable.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_mov_carry_users", WiredExtraMoveCarryUsers.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_mov_no_animation", WiredExtraMoveNoAnimation.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_anim_time", WiredExtraAnimationTime.class)); @@ -357,6 +381,16 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_xtra_execution_limit", WiredExtraExecutionLimit.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_text_output_username", WiredExtraTextOutputUsername.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_text_output_furni_name", WiredExtraTextOutputFurniName.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_text_output_variable", WiredExtraTextOutputVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_text_input_variable", WiredExtraTextInputVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_var_text_connector", WiredExtraVariableTextConnector.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_var_lvlup_system", WiredExtraVariableLevelUpSystem.class)); + this.interactionsList.add(new ItemInteraction("wf_var_user", WiredExtraUserVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_furni", WiredExtraFurniVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_room", WiredExtraRoomVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_context", WiredExtraContextVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_reference", WiredExtraVariableReference.class)); + this.interactionsList.add(new ItemInteraction("wf_var_echo", WiredExtraVariableEcho.class)); this.interactionsList.add(new ItemInteraction("wf_highscore", InteractionWiredHighscore.class)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java index 97018e8b..8af6b046 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java @@ -43,7 +43,7 @@ public abstract class InteractionWiredCondition extends InteractionWired impleme @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { client.sendResponse(new WiredConditionDataComposer(this, room)); this.activateBox(room); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java index 9bf8a905..a71e2820 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java @@ -80,7 +80,7 @@ public abstract class InteractionWiredEffect extends InteractionWired implements @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { client.sendResponse(new WiredEffectDataComposer(this, room)); this.activateBox(room); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java index 727684f8..18ba973f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java @@ -23,7 +23,7 @@ public abstract class InteractionWiredExtra extends InteractionWired { @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { if (this.hasConfiguration()) { client.sendResponse(new WiredExtraDataComposer(this, room)); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java index 16e5b4c8..24c67767 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java @@ -45,7 +45,7 @@ public abstract class InteractionWiredTrigger extends InteractionWired implement @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { client.sendResponse(new WiredTriggerDataComposer(this, room)); this.activateBox(room); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasVariable.java new file mode 100644 index 00000000..65349024 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasVariable.java @@ -0,0 +1,506 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredConditionHasVariable extends InteractionWiredCondition { + private static final Logger LOGGER = LoggerFactory.getLogger(WiredConditionHasVariable.class); + + protected static final int TARGET_USER = 0; + protected static final int TARGET_FURNI = 1; + protected static final int TARGET_CONTEXT = 2; + protected static final int TARGET_ROOM = 3; + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + + public static final WiredConditionType type = WiredConditionType.HAS_VAR; + + protected final THashSet selectedItems = new THashSet<>(); + protected int targetType = TARGET_USER; + protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int quantifier = QUANTIFIER_ALL; + protected String variableToken = ""; + protected int variableItemId = 0; + + public WiredConditionHasVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionHasVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(); + + List serializedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + serializedItems.addAll(this.selectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken == null ? "" : this.variableToken); + message.appendInt(4); + message.appendInt(this.targetType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + this.targetType = (params.length > 0) ? normalizeTargetType(params[0]) : TARGET_USER; + this.userSource = (params.length > 1) ? normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 2) ? normalizeFurniSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 3) ? normalizeQuantifier(params[3]) : QUANTIFIER_ALL; + this.setVariableToken(normalizeVariableToken(settings.getStringParam())); + + if (this.variableToken.isEmpty()) { + return false; + } + + this.selectedItems.clear(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED && room != null) { + int[] furniIds = settings.getFurniIds(); + if (furniIds.length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int furniId : furniIds) { + HabboItem item = room.getHabboItem(furniId); + + if (item != null) { + this.selectedItems.add(item); + } + } + } + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + return this.evaluateWithNegation(ctx, false); + } + + protected boolean evaluateWithNegation(WiredContext ctx, boolean negative) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { + return false; + } + + return switch (this.targetType) { + case TARGET_FURNI -> this.evaluateFurniTargets(ctx, room, negative); + case TARGET_CONTEXT -> { + boolean contextMatch = this.matchesContext(ctx, room); + yield negative ? !contextMatch : contextMatch; + } + case TARGET_ROOM -> { + boolean roomMatch = this.matchesRoom(room); + yield negative ? !roomMatch : roomMatch; + } + default -> this.evaluateUserTargets(ctx, room, negative); + }; + } + + private boolean evaluateUserTargets(WiredContext ctx, Room room, boolean negative) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) return false; + + boolean match = (this.quantifier == QUANTIFIER_ANY) + ? this.matchesAnyUser(room, targets) + : this.matchesAllUsers(room, targets); + + return negative ? !match : match; + } + + private boolean evaluateFurniTargets(WiredContext ctx, Room room, boolean negative) { + this.refresh(); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedItems); + if (targets.isEmpty()) return false; + + boolean match = (this.quantifier == QUANTIFIER_ANY) + ? this.matchesAnyFurni(room, targets) + : this.matchesAllFurni(room, targets); + + return negative ? !match : match; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + this.refresh(); + + List itemIds = new ArrayList<>(); + for (HabboItem item : this.selectedItems) { + if (item != null) itemIds.add(item.getId()); + } + + return WiredManager.getGson().toJson(new JsonData( + itemIds, + this.targetType, + this.variableToken, + this.variableItemId, + this.userSource, + this.furniSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) return; + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) return; + + this.targetType = normalizeTargetType(data.targetType); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.quantifier = normalizeQuantifier(data.quantifier); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + + if (room != null && data.itemIds != null) { + for (Integer itemId : data.itemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.selectedItems.add(item); + } + } + + return; + } + + this.setVariableToken(normalizeVariableToken(wiredData)); + } catch (Exception e) { + LOGGER.error("Failed to load wired variable condition data for item {}", this.getId(), e); + this.onPickUp(); + } + } + + @Override + public void onPickUp() { + this.targetType = TARGET_USER; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + this.selectedItems.clear(); + this.setVariableToken(""); + } + + protected boolean matchesAnyUser(Room room, List targets) { + for (RoomUnit roomUnit : targets) { + if (this.matchesUser(room, roomUnit)) { + return true; + } + } + + return false; + } + + protected boolean matchesAllUsers(Room room, List targets) { + for (RoomUnit roomUnit : targets) { + if (!this.matchesUser(room, roomUnit)) { + return false; + } + } + + return true; + } + + protected boolean matchesAnyFurni(Room room, List targets) { + for (HabboItem item : targets) { + if (this.matchesFurni(room, item)) { + return true; + } + } + + return false; + } + + protected boolean matchesAllFurni(Room room, List targets) { + for (HabboItem item : targets) { + if (!this.matchesFurni(room, item)) { + return false; + } + } + + return true; + } + + protected boolean matchesUser(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + Habbo habbo = room.getHabbo(roomUnit); + + return habbo != null && room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return this.hasUserInternalVariable(room, roomUnit, getInternalVariableKey(this.variableToken)); + } + + return false; + } + + protected boolean matchesFurni(Room room, HabboItem item) { + if (room == null || item == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + return room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return this.hasFurniInternalVariable(item, getInternalVariableKey(this.variableToken)); + } + + return false; + } + + protected boolean matchesContext(WiredContext ctx, Room room) { + if (ctx == null || room == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + return WiredContextVariableSupport.hasVariable(ctx, this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return WiredInternalVariableSupport.readContextValue(ctx, getInternalVariableKey(this.variableToken)) != null; + } + + return false; + } + + protected boolean matchesRoom(Room room) { + if (room == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + return room.getRoomVariableManager().hasVariable(this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return this.hasRoomInternalVariable(getInternalVariableKey(this.variableToken)); + } + + return false; + } + + protected boolean hasUserInternalVariable(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.hasUserValue(room, roomUnit, key); + } + + protected boolean hasFurniInternalVariable(HabboItem item, String key) { + return WiredInternalVariableSupport.hasFurniValue(item, key); + } + + protected boolean hasRoomInternalVariable(String key) { + return WiredInternalVariableSupport.hasRoomValue(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()), key); + } + + protected void refresh() { + THashSet staleItems = new THashSet<>(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + staleItems.addAll(this.selectedItems); + } else { + for (HabboItem item : this.selectedItems) { + if (item == null || item.getRoomId() != room.getId()) { + staleItems.add(item); + } + } + } + + this.selectedItems.removeAll(staleItems); + } + + protected void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + protected boolean hasRoomEntryMethod(Habbo habbo) { + if (habbo == null) return false; + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + + return roomEntryMethod != null && !roomEntryMethod.trim().isEmpty() && !"unknown".equalsIgnoreCase(roomEntryMethod); + } + + protected TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + protected static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + protected static int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + protected static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + protected static int normalizeFurniSource(int value) { + return switch (value) { + case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + protected static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + protected static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + protected static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException e) { + return 0; + } + } + + protected static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + protected static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (isCustomVariableToken(normalized)) return normalized; + if (isInternalVariableToken(normalized)) return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + + try { + int parsed = Integer.parseInt(normalized); + return (parsed > 0) ? (CUSTOM_TOKEN_PREFIX + parsed) : ""; + } catch (NumberFormatException e) { + return ""; + } + } + + protected static class JsonData { + List itemIds; + int targetType; + String variableToken; + int variableItemId; + int userSource; + int furniSource; + int quantifier; + + public JsonData(List itemIds, int targetType, String variableToken, int variableItemId, int userSource, int furniSource, int quantifier) { + this.itemIds = itemIds; + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.userSource = userSource; + this.furniSource = furniSource; + this.quantifier = quantifier; + } + } + + protected static class TeamEffectData { + final int colorId; + final int typeId; + + protected TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHasVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHasVariable.java new file mode 100644 index 00000000..c0dcc43f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHasVariable.java @@ -0,0 +1,30 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredConditionNotHasVariable extends WiredConditionHasVariable { + public static final WiredConditionType type = WiredConditionType.NOT_HAS_VAR; + + public WiredConditionNotHasVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionNotHasVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public boolean evaluate(WiredContext ctx) { + return this.evaluateWithNegation(ctx, true); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java new file mode 100644 index 00000000..18456674 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java @@ -0,0 +1,420 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { + public static final WiredConditionType type = WiredConditionType.VAR_AGE_MATCH; + + private static final int TARGET_CONTEXT = 2; + private static final int COMPARE_VALUE_CREATED = 0; + private static final int COMPARE_VALUE_UPDATED = 1; + private static final int COMPARISON_LOWER_THAN = 0; + private static final int COMPARISON_HIGHER_THAN = 2; + private static final int DURATION_UNIT_MILLISECONDS = 0; + private static final int DURATION_UNIT_SECONDS = 1; + private static final int DURATION_UNIT_MINUTES = 2; + private static final int DURATION_UNIT_HOURS = 3; + private static final int DURATION_UNIT_DAYS = 4; + private static final int DURATION_UNIT_WEEKS = 5; + private static final int DURATION_UNIT_MONTHS = 6; + private static final int DURATION_UNIT_YEARS = 7; + + protected int compareValue = COMPARE_VALUE_CREATED; + protected int comparison = COMPARISON_LOWER_THAN; + protected int durationAmount = 0; + protected int durationUnit = DURATION_UNIT_SECONDS; + + public WiredConditionVariableAgeMatch(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionVariableAgeMatch(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(); + + List serializedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + serializedItems.addAll(this.selectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken == null ? "" : this.variableToken); + message.appendInt(8); + message.appendInt(this.targetType); + message.appendInt(this.compareValue); + message.appendInt(this.comparison); + message.appendInt(this.durationAmount); + message.appendInt(this.durationUnit); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + this.targetType = (params.length > 0) ? normalizeTargetTypeExtended(params[0]) : TARGET_USER; + this.compareValue = (params.length > 1) ? normalizeCompareValue(params[1]) : COMPARE_VALUE_CREATED; + this.comparison = (params.length > 2) ? normalizeComparison(params[2]) : COMPARISON_LOWER_THAN; + this.durationAmount = Math.max(0, (params.length > 3) ? params[3] : 0); + this.durationUnit = (params.length > 4) ? normalizeDurationUnit(params[4]) : DURATION_UNIT_SECONDS; + this.userSource = (params.length > 5) ? normalizeUserSource(params[5]) : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 6) ? normalizeFurniSource(params[6]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 7) ? normalizeQuantifier(params[7]) : QUANTIFIER_ALL; + this.setVariableToken(normalizeVariableToken(settings.getStringParam())); + + if (!this.isValidSource(room)) { + return false; + } + + this.selectedItems.clear(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED && room != null) { + int[] furniIds = settings.getFurniIds(); + if (furniIds.length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int furniId : furniIds) { + HabboItem item = room.getHabboItem(furniId); + + if (item != null) { + this.selectedItems.add(item); + } + } + } + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty() || !isCustomVariableToken(this.variableToken)) { + return false; + } + + long thresholdMs = durationToMillis(this.durationAmount, this.durationUnit); + + return switch (this.targetType) { + case TARGET_FURNI -> this.evaluateFurniTargets(ctx, room, thresholdMs); + case TARGET_ROOM -> this.evaluateRoomTarget(room, thresholdMs); + case TARGET_CONTEXT -> this.evaluateContextTarget(ctx, room, thresholdMs); + default -> this.evaluateUserTargets(ctx, room, thresholdMs); + }; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + this.refresh(); + + List itemIds = new ArrayList<>(); + for (HabboItem item : this.selectedItems) { + if (item != null) itemIds.add(item.getId()); + } + + return WiredManager.getGson().toJson(new JsonData( + itemIds, + this.targetType, + this.variableToken, + this.variableItemId, + this.compareValue, + this.comparison, + this.durationAmount, + this.durationUnit, + this.userSource, + this.furniSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) return; + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) return; + + this.targetType = normalizeTargetTypeExtended(data.targetType); + this.compareValue = normalizeCompareValue(data.compareValue); + this.comparison = normalizeComparison(data.comparison); + this.durationAmount = Math.max(0, data.durationAmount); + this.durationUnit = normalizeDurationUnit(data.durationUnit); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.quantifier = normalizeQuantifier(data.quantifier); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + + if (room != null && data.itemIds != null) { + for (Integer itemId : data.itemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.selectedItems.add(item); + } + } + + return; + } + + this.setVariableToken(normalizeVariableToken(wiredData)); + } catch (Exception e) { + this.onPickUp(); + } + } + + @Override + public void onPickUp() { + super.onPickUp(); + this.compareValue = COMPARE_VALUE_CREATED; + this.comparison = COMPARISON_LOWER_THAN; + this.durationAmount = 0; + this.durationUnit = DURATION_UNIT_SECONDS; + } + + private boolean evaluateUserTargets(WiredContext ctx, Room room, long thresholdMs) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) return false; + + if (this.quantifier == QUANTIFIER_ANY) { + for (RoomUnit roomUnit : targets) { + if (this.matchesAge(this.readUserAgeMs(room, roomUnit), thresholdMs)) return true; + } + + return false; + } + + for (RoomUnit roomUnit : targets) { + if (!this.matchesAge(this.readUserAgeMs(room, roomUnit), thresholdMs)) return false; + } + + return true; + } + + private boolean evaluateFurniTargets(WiredContext ctx, Room room, long thresholdMs) { + this.refresh(); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedItems); + if (targets.isEmpty()) return false; + + if (this.quantifier == QUANTIFIER_ANY) { + for (HabboItem item : targets) { + if (this.matchesAge(this.readFurniAgeMs(room, item), thresholdMs)) return true; + } + + return false; + } + + for (HabboItem item : targets) { + if (!this.matchesAge(this.readFurniAgeMs(room, item), thresholdMs)) return false; + } + + return true; + } + + private boolean evaluateRoomTarget(Room room, long thresholdMs) { + return this.matchesAge(this.readRoomAgeMs(room), thresholdMs); + } + + private boolean evaluateContextTarget(WiredContext ctx, Room room, long thresholdMs) { + return this.matchesAge(this.readContextAgeMs(ctx, room), thresholdMs); + } + + private Long readUserAgeMs(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null || !room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId)) return null; + + int timestamp = (this.compareValue == COMPARE_VALUE_UPDATED) + ? room.getUserVariableManager().getUpdatedAt(habbo.getHabboInfo().getId(), this.variableItemId) + : room.getUserVariableManager().getCreatedAt(habbo.getHabboInfo().getId(), this.variableItemId); + + return timestampToAgeMs(timestamp); + } + + private Long readFurniAgeMs(Room room, HabboItem item) { + if (room == null || item == null || !room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId)) return null; + + int timestamp = (this.compareValue == COMPARE_VALUE_UPDATED) + ? room.getFurniVariableManager().getUpdatedAt(item.getId(), this.variableItemId) + : room.getFurniVariableManager().getCreatedAt(item.getId(), this.variableItemId); + + return timestampToAgeMs(timestamp); + } + + private Long readRoomAgeMs(Room room) { + if (room == null) return null; + if (this.compareValue == COMPARE_VALUE_CREATED) return null; + + int timestamp = room.getRoomVariableManager().getUpdatedAt(this.variableItemId); + return timestampToAgeMs(timestamp); + } + + private Long readContextAgeMs(WiredContext ctx, Room room) { + if (ctx == null || room == null || !WiredContextVariableSupport.hasVariable(ctx, this.variableItemId)) return null; + + int timestamp = (this.compareValue == COMPARE_VALUE_UPDATED) + ? WiredContextVariableSupport.getUpdatedAt(ctx, this.variableItemId) + : WiredContextVariableSupport.getCreatedAt(ctx, this.variableItemId); + + return timestampToAgeMs(timestamp); + } + + private boolean matchesAge(Long ageMs, long thresholdMs) { + if (ageMs == null) return false; + + return switch (this.comparison) { + case COMPARISON_HIGHER_THAN -> ageMs > thresholdMs; + default -> ageMs < thresholdMs; + }; + } + + private boolean isValidSource(Room room) { + if (room == null || !isCustomVariableToken(this.variableToken)) return false; + + return switch (this.targetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getDefinitionInfo(this.variableItemId) != null; + case TARGET_CONTEXT -> WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId) != null; + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.variableItemId); + yield this.compareValue == COMPARE_VALUE_UPDATED && definition != null; + } + default -> room.getUserVariableManager().getDefinitionInfo(this.variableItemId) != null; + }; + } + + private static Long timestampToAgeMs(int timestampSeconds) { + if (timestampSeconds <= 0) return null; + + long timestampMs = (timestampSeconds * 1000L); + return Math.max(0L, System.currentTimeMillis() - timestampMs); + } + + private static long durationToMillis(int amount, int unit) { + long normalizedAmount = Math.max(0L, amount); + + return switch (unit) { + case DURATION_UNIT_MILLISECONDS -> normalizedAmount; + case DURATION_UNIT_MINUTES -> safeMultiply(normalizedAmount, 60_000L); + case DURATION_UNIT_HOURS -> safeMultiply(normalizedAmount, 3_600_000L); + case DURATION_UNIT_DAYS -> safeMultiply(normalizedAmount, 86_400_000L); + case DURATION_UNIT_WEEKS -> safeMultiply(normalizedAmount, 604_800_000L); + case DURATION_UNIT_MONTHS -> safeMultiply(normalizedAmount, 2_592_000_000L); + case DURATION_UNIT_YEARS -> safeMultiply(normalizedAmount, 31_536_000_000L); + default -> safeMultiply(normalizedAmount, 1_000L); + }; + } + + private static long safeMultiply(long left, long right) { + if (left <= 0 || right <= 0) return 0L; + if (left > (Long.MAX_VALUE / right)) return Long.MAX_VALUE; + + return left * right; + } + + private static int normalizeTargetTypeExtended(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeCompareValue(int value) { + return (value == COMPARE_VALUE_UPDATED) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED; + } + + private static int normalizeComparison(int value) { + return (value == COMPARISON_HIGHER_THAN) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN; + } + + private static int normalizeDurationUnit(int value) { + return switch (value) { + case DURATION_UNIT_MILLISECONDS, DURATION_UNIT_SECONDS, DURATION_UNIT_MINUTES, DURATION_UNIT_HOURS, + DURATION_UNIT_DAYS, DURATION_UNIT_WEEKS, DURATION_UNIT_MONTHS, DURATION_UNIT_YEARS -> value; + default -> DURATION_UNIT_SECONDS; + }; + } + + protected static class JsonData { + List itemIds; + int targetType; + String variableToken; + int variableItemId; + int compareValue; + int comparison; + int durationAmount; + int durationUnit; + int userSource; + int furniSource; + int quantifier; + + JsonData(List itemIds, int targetType, String variableToken, int variableItemId, int compareValue, int comparison, int durationAmount, int durationUnit, int userSource, int furniSource, int quantifier) { + this.itemIds = itemIds; + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.compareValue = compareValue; + this.comparison = comparison; + this.durationAmount = durationAmount; + this.durationUnit = durationUnit; + this.userSource = userSource; + this.furniSource = furniSource; + this.quantifier = quantifier; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java new file mode 100644 index 00000000..13f79233 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java @@ -0,0 +1,814 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.util.HotelDateTimeUtil; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; + +public class WiredConditionVariableValueMatch extends WiredConditionHasVariable { + public static final WiredConditionType type = WiredConditionType.VAR_VAL_MATCH; + + private static final int TARGET_CONTEXT = 2; + private static final int SOURCE_SECONDARY_SELECTED = 101; + private static final int REFERENCE_CONSTANT = 0; + private static final int REFERENCE_VARIABLE = 1; + private static final int COMPARISON_GREATER_THAN = 0; + private static final int COMPARISON_GREATER_THAN_OR_EQUAL = 1; + private static final int COMPARISON_EQUAL = 2; + private static final int COMPARISON_LESS_THAN_OR_EQUAL = 3; + private static final int COMPARISON_LESS_THAN = 4; + private static final int COMPARISON_NOT_EQUAL = 5; + private static final String DELIM = "\t"; + private static final String FURNI_DELIM = ";"; + + protected int comparison = COMPARISON_EQUAL; + protected int referenceMode = REFERENCE_CONSTANT; + protected int referenceConstantValue = 0; + protected int referenceTargetType = TARGET_USER; + protected int referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected String referenceVariableToken = ""; + protected int referenceVariableItemId = 0; + protected final THashSet referenceSelectedItems = new THashSet<>(); + + public WiredConditionVariableValueMatch(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionVariableValueMatch(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(); + this.refreshReferenceItems(); + + List serializedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + serializedItems.addAll(this.selectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(10); + message.appendInt(this.targetType); + message.appendInt(this.comparison); + message.appendInt(this.referenceMode); + message.appendInt(this.referenceConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) return false; + + int[] params = settings.getIntParams(); + String[] stringParts = this.parseStringData(settings.getStringParam()); + int nextTargetType = normalizeTargetTypeExtended(param(params, 0, TARGET_USER)); + int nextComparison = normalizeComparison(param(params, 1, COMPARISON_EQUAL)); + int nextReferenceMode = normalizeReferenceMode(param(params, 2, REFERENCE_CONSTANT)); + int nextReferenceConstantValue = param(params, 3, 0); + int nextReferenceTargetType = normalizeTargetTypeExtended(param(params, 4, TARGET_USER)); + int nextUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + int nextFurniSource = normalizeFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 7, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 8, WiredSourceUtil.SOURCE_TRIGGER)); + int nextQuantifier = normalizeQuantifier(param(params, 9, QUANTIFIER_ALL)); + String nextVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : settings.getStringParam()); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + + if (!this.isValidSource(room, nextTargetType, nextVariableToken)) return false; + if (nextReferenceMode == REFERENCE_VARIABLE && !this.isValidReference(room, nextReferenceTargetType, nextReferenceVariableToken)) return false; + + int selectionLimit = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); + List nextSelectedItems = (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) + ? this.parseItems(settings.getFurniIds(), room) + : new ArrayList<>(); + List nextReferenceItems = (nextReferenceMode == REFERENCE_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) + ? this.parseItems((stringParts.length > 2) ? stringParts[2] : "", room) + : new ArrayList<>(); + + if (nextSelectedItems.size() > selectionLimit || nextReferenceItems.size() > selectionLimit) return false; + + this.selectedItems.clear(); + this.selectedItems.addAll(nextSelectedItems); + this.referenceSelectedItems.clear(); + this.referenceSelectedItems.addAll(nextReferenceItems); + this.targetType = nextTargetType; + this.comparison = nextComparison; + this.referenceMode = nextReferenceMode; + this.referenceConstantValue = nextReferenceConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.userSource = nextUserSource; + this.furniSource = nextFurniSource; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.quantifier = nextQuantifier; + this.setVariableToken(nextVariableToken); + this.setReferenceVariableToken(nextReferenceVariableToken); + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { + return false; + } + + return switch (this.targetType) { + case TARGET_FURNI -> this.evaluateFurniTargets(ctx, room); + case TARGET_ROOM -> this.evaluateRoomTarget(ctx, room); + case TARGET_CONTEXT -> this.evaluateContextTarget(ctx, room); + default -> this.evaluateUserTargets(ctx, room); + }; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + this.refresh(); + this.refreshReferenceItems(); + + return WiredManager.getGson().toJson(new JsonData( + this.targetType, + this.variableToken, + this.variableItemId, + this.comparison, + this.referenceMode, + this.referenceConstantValue, + this.referenceTargetType, + this.referenceVariableToken, + this.referenceVariableItemId, + this.userSource, + this.furniSource, + this.referenceUserSource, + this.referenceFurniSource, + this.quantifier, + this.toIds(this.selectedItems), + this.toIds(this.referenceSelectedItems) + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.targetType = normalizeTargetTypeExtended(data.targetType); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.comparison = normalizeComparison(data.comparison); + this.referenceMode = normalizeReferenceMode(data.referenceMode); + this.referenceConstantValue = data.referenceConstantValue; + this.referenceTargetType = normalizeTargetTypeExtended(data.referenceTargetType); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.quantifier = normalizeQuantifier(data.quantifier); + + if (room == null) return; + + this.selectedItems.addAll(this.parseItems(data.selectedItemIds, room)); + this.referenceSelectedItems.addAll(this.parseItems(data.referenceSelectedItemIds, room)); + } + + @Override + public void onPickUp() { + super.onPickUp(); + this.comparison = COMPARISON_EQUAL; + this.referenceMode = REFERENCE_CONSTANT; + this.referenceConstantValue = 0; + this.referenceTargetType = TARGET_USER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceSelectedItems.clear(); + this.setReferenceVariableToken(""); + } + + public boolean requiresTriggeringUser() { + return (this.targetType == TARGET_USER && this.userSource == WiredSourceUtil.SOURCE_TRIGGER) + || (this.referenceMode == REFERENCE_VARIABLE && this.referenceTargetType == TARGET_USER && this.referenceUserSource == WiredSourceUtil.SOURCE_TRIGGER); + } + + private boolean evaluateUserTargets(WiredContext ctx, Room room) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) return false; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + + if (this.quantifier == QUANTIFIER_ANY) { + int index = 0; + for (RoomUnit roomUnit : targets) { + Integer currentValue = this.readUserValue(room, roomUnit); + Integer referenceValue = this.referenceFor(references, roomUnit != null ? roomUnit.getId() : 0, TARGET_USER, index++); + + if (this.matchesComparison(currentValue, referenceValue)) return true; + } + + return false; + } + + int index = 0; + for (RoomUnit roomUnit : targets) { + Integer currentValue = this.readUserValue(room, roomUnit); + Integer referenceValue = this.referenceFor(references, roomUnit != null ? roomUnit.getId() : 0, TARGET_USER, index++); + + if (!this.matchesComparison(currentValue, referenceValue)) return false; + } + + return true; + } + + private boolean evaluateFurniTargets(WiredContext ctx, Room room) { + this.refresh(); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedItems); + if (targets.isEmpty()) return false; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + + if (this.quantifier == QUANTIFIER_ANY) { + int index = 0; + for (HabboItem item : targets) { + Integer currentValue = this.readFurniValue(room, item); + Integer referenceValue = this.referenceFor(references, item != null ? item.getId() : 0, TARGET_FURNI, index++); + + if (this.matchesComparison(currentValue, referenceValue)) return true; + } + + return false; + } + + int index = 0; + for (HabboItem item : targets) { + Integer currentValue = this.readFurniValue(room, item); + Integer referenceValue = this.referenceFor(references, item != null ? item.getId() : 0, TARGET_FURNI, index++); + + if (!this.matchesComparison(currentValue, referenceValue)) return false; + } + + return true; + } + + private boolean evaluateRoomTarget(WiredContext ctx, Room room) { + Integer currentValue = this.readRoomValue(room); + Integer referenceValue = this.referenceFor(this.resolveReferences(ctx, room), room.getId(), TARGET_ROOM, 0); + + return this.matchesComparison(currentValue, referenceValue); + } + + private boolean evaluateContextTarget(WiredContext ctx, Room room) { + Integer currentValue = this.readContextTargetValue(ctx, room); + Integer referenceValue = this.referenceFor(this.resolveReferences(ctx, room), this.variableItemId, TARGET_CONTEXT, 0); + + return this.matchesComparison(currentValue, referenceValue); + } + + private ReferenceSnapshot resolveReferences(WiredContext ctx, Room room) { + if (this.referenceMode != REFERENCE_VARIABLE) return null; + + return switch (this.referenceTargetType) { + case TARGET_USER -> this.userReferences(ctx, room); + case TARGET_FURNI -> this.furniReferences(ctx, room); + case TARGET_CONTEXT -> this.contextReferences(ctx, room); + case TARGET_ROOM -> this.roomReferences(room); + default -> null; + }; + } + + private ReferenceSnapshot userReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_USER); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseUserInternalReference(key)) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + Integer value = this.readUserInternalValue(room, roomUnit, key); + if (value != null && roomUnit != null) snapshot.add(roomUnit.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) snapshot.add(roomUnit.getId(), room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot furniReferences(WiredContext ctx, Room room) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.refreshReferenceItems(); + + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_FURNI); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseFurniInternalReference(key)) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + Integer value = this.readFurniInternalValue(room, item, key); + if (value != null && item != null) snapshot.add(item.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + if (item != null) snapshot.add(item.getId(), room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot roomReferences(Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_ROOM); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseRoomInternalReference(key)) return null; + + Integer value = this.readRoomInternalValue(room, key); + if (value == null) return null; + + snapshot.add(room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + snapshot.add(room.getId(), room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId)); + return snapshot; + } + + private ReferenceSnapshot contextReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_CONTEXT); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseContextInternalReference(key)) return null; + + Integer value = WiredInternalVariableSupport.readContextValue(ctx, key); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId > 0 ? this.referenceVariableItemId : (room != null ? room.getId() : 0), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId, value); + return snapshot; + } + + private Integer readUserValue(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + if (definition == null || !definition.hasValue()) return null; + + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null) ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.variableItemId) : null; + } + + private Integer readFurniValue(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + return (definition != null && definition.hasValue()) ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.variableItemId) : null; + } + + private Integer readRoomValue(Room room) { + if (room == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseRoomInternalReference(key) ? this.readRoomInternalValue(room, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.variableItemId); + return (definition != null && definition.hasValue()) ? room.getRoomVariableManager().getCurrentValue(this.variableItemId) : null; + } + + private Integer readContextTargetValue(WiredContext ctx, Room room) { + if (ctx == null || room == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseContextInternalReference(key) ? WiredInternalVariableSupport.readContextValue(ctx, key) : null; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.variableItemId)) return null; + + return WiredContextVariableSupport.getCurrentValue(ctx, this.variableItemId); + } + + private Integer referenceFor(ReferenceSnapshot snapshot, int destinationEntityId, int destinationTarget, int destinationIndex) { + if (this.referenceMode != REFERENCE_VARIABLE) return this.referenceConstantValue; + if (snapshot == null || snapshot.isEmpty()) return null; + if (snapshot.targetType == destinationTarget && snapshot.values.containsKey(destinationEntityId)) return snapshot.values.get(destinationEntityId); + if (destinationIndex >= 0 && destinationIndex < snapshot.values.size()) return new ArrayList<>(snapshot.values.values()).get(destinationIndex); + return new ArrayList<>(snapshot.values.values()).get(0); + } + + private boolean matchesComparison(Integer currentValue, Integer referenceValue) { + if (currentValue == null || referenceValue == null) return false; + + return switch (this.comparison) { + case COMPARISON_GREATER_THAN -> currentValue > referenceValue; + case COMPARISON_GREATER_THAN_OR_EQUAL -> currentValue >= referenceValue; + case COMPARISON_LESS_THAN_OR_EQUAL -> currentValue <= referenceValue; + case COMPARISON_LESS_THAN -> currentValue < referenceValue; + case COMPARISON_NOT_EQUAL -> !currentValue.equals(referenceValue); + default -> currentValue.equals(referenceValue); + }; + } + + private boolean isValidSource(Room room, int targetType, String variableToken) { + if (variableToken == null || variableToken.isEmpty()) return false; + + return switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalReference(getInternalVariableKey(variableToken)) + : this.isValidUserCustomValue(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalReference(getInternalVariableKey(variableToken)) + : this.isValidFurniCustomValue(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> isInternalVariableToken(variableToken) + ? canUseContextInternalReference(getInternalVariableKey(variableToken)) + : this.isValidContextCustomValue(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> isInternalVariableToken(variableToken) + ? canUseRoomInternalReference(getInternalVariableKey(variableToken)) + : this.isValidRoomCustomValue(room, getCustomItemId(variableToken)); + default -> false; + }; + } + + private boolean isValidReference(Room room, int targetType, String variableToken) { + return this.isValidSource(room, targetType, variableToken); + } + + private boolean isValidUserCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidFurniCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidRoomCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidContextCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private Integer getTeamColorId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private Integer getTeamTypeId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private List parseItems(int[] ids, Room room) { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (int id : ids) { + HabboItem item = room.getHabboItem(id); + if (item != null) items.add(item); + } + + return items; + } + + private List parseItems(List ids, Room room) { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (Integer id : ids) { + if (id == null || id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item != null) items.add(item); + } + + return items; + } + + private List parseItems(String ids, Room room) { + List items = new ArrayList<>(); + if (ids == null || ids.trim().isEmpty() || room == null) return items; + + for (String part : ids.split("[;,\\t]")) { + int id = parseInteger(part); + if (id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item != null) items.add(item); + } + + return items; + } + + private void refreshReferenceItems() { + THashSet staleItems = new THashSet<>(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + staleItems.addAll(this.referenceSelectedItems); + } else { + for (HabboItem item : this.referenceSelectedItems) { + if (item == null || item.getRoomId() != room.getId() || room.getHabboItem(item.getId()) == null) { + staleItems.add(item); + } + } + } + + this.referenceSelectedItems.removeAll(staleItems); + } + + private String serializeStringData() { + return (this.variableToken == null ? "" : this.variableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken) + DELIM + this.serializeIds(this.referenceSelectedItems); + } + + private String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private List toIds(THashSet items) { + List ids = new ArrayList<>(); + for (HabboItem item : items) { + if (item != null) ids.add(item.getId()); + } + return ids; + } + + private String serializeIds(THashSet items) { + StringBuilder builder = new StringBuilder(); + + for (HabboItem item : items) { + if (item == null) continue; + if (builder.length() > 0) builder.append(FURNI_DELIM); + builder.append(item.getId()); + } + + return builder.toString(); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params.length > index) ? params[index] : fallback; + } + + private static int normalizeTargetTypeExtended(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeReferenceMode(int value) { + return (value == REFERENCE_VARIABLE) ? REFERENCE_VARIABLE : REFERENCE_CONSTANT; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeComparison(int value) { + return switch (value) { + case COMPARISON_GREATER_THAN, COMPARISON_GREATER_THAN_OR_EQUAL, COMPARISON_LESS_THAN_OR_EQUAL, COMPARISON_LESS_THAN, COMPARISON_NOT_EQUAL -> value; + default -> COMPARISON_EQUAL; + }; + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + static class JsonData { + int targetType, variableItemId, comparison, referenceMode, referenceConstantValue, referenceTargetType, referenceVariableItemId, userSource, furniSource, referenceUserSource, referenceFurniSource, quantifier; + String variableToken, referenceVariableToken; + List selectedItemIds, referenceSelectedItemIds; + + JsonData(int targetType, String variableToken, int variableItemId, int comparison, int referenceMode, int referenceConstantValue, int referenceTargetType, String referenceVariableToken, int referenceVariableItemId, int userSource, int furniSource, int referenceUserSource, int referenceFurniSource, int quantifier, List selectedItemIds, List referenceSelectedItemIds) { + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.comparison = comparison; + this.referenceMode = referenceMode; + this.referenceConstantValue = referenceConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.userSource = userSource; + this.furniSource = furniSource; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.quantifier = quantifier; + this.selectedItemIds = selectedItemIds; + this.referenceSelectedItemIds = referenceSelectedItemIds; + } + } + + private static class ReferenceSnapshot { + final int targetType; + final LinkedHashMap values = new LinkedHashMap<>(); + + ReferenceSnapshot(int targetType) { + this.targetType = targetType; + } + + void add(int entityId, int value) { + this.values.put(entityId, value); + } + + boolean isEmpty() { + return this.values.isEmpty(); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java new file mode 100644 index 00000000..81fe314a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java @@ -0,0 +1,932 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; + +public class WiredEffectChangeVariableValue extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.CHANGE_VAR_VAL; + public static final int TARGET_USER = 0, TARGET_FURNI = 1, TARGET_CONTEXT = 2, TARGET_ROOM = 3; + public static final int REF_CONSTANT = 0, REF_VARIABLE = 1; + public static final int OP_ASSIGN = 0, OP_ADD = 1, OP_SUB = 2, OP_MUL = 3, OP_DIV = 4, OP_POW = 5, OP_MOD = 6, OP_MIN = 40, OP_MAX = 41, OP_RANDOM = 50, OP_ABS = 60, OP_AND = 100, OP_OR = 101, OP_XOR = 102, OP_NOT = 103, OP_LSHIFT = 104, OP_RSHIFT = 105; + + private static final int SOURCE_SECONDARY_SELECTED = 101; + private static final String DELIM = "\t", FURNI_DELIM = ";"; + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + + private int destinationTargetType = TARGET_USER, destinationVariableItemId = 0, operation = OP_ASSIGN, referenceMode = REF_CONSTANT, referenceConstantValue = 0, referenceTargetType = TARGET_USER, referenceVariableItemId = 0, destinationUserSource = WiredSourceUtil.SOURCE_TRIGGER, destinationFurniSource = WiredSourceUtil.SOURCE_TRIGGER, referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER, referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + private String destinationVariableToken = "", referenceVariableToken = ""; + private final List destinationSelectedFurni = new ArrayList<>(); + private final List referenceSelectedFurni = new ArrayList<>(); + + public WiredEffectChangeVariableValue(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectChangeVariableValue(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) return; + + switch (this.destinationTargetType) { + case TARGET_USER -> this.executeUsers(ctx, room); + case TARGET_FURNI -> this.executeFurni(ctx, room); + case TARGET_CONTEXT -> this.executeContext(ctx, room); + case TARGET_ROOM -> this.executeRoom(ctx, room); + } + } + + private void executeUsers(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) { + this.executeUsersInternal(ctx, room, getInternalVariableKey(this.destinationVariableToken)); + return; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.destinationUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null) continue; + + Integer referenceValue = this.referenceFor(references, roomUnit.getId(), TARGET_USER, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + int currentValue = room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.destinationVariableItemId); + room.getUserVariableManager().updateVariableValue(habbo.getHabboInfo().getId(), this.destinationVariableItemId, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeUsersInternal(WiredContext ctx, Room room, String key) { + if (!canUseUserInternalDestination(key)) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.destinationUserSource)) { + if (roomUnit == null) continue; + + Integer currentValue = this.readUserInternalValue(room, roomUnit, key); + if (currentValue == null) continue; + + Integer referenceValue = this.referenceFor(references, roomUnit.getId(), TARGET_USER, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + this.writeUserInternalValue(room, roomUnit, key, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeFurni(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) { + this.executeFurniInternal(ctx, room, getInternalVariableKey(this.destinationVariableToken)); + return; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + if (this.destinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) this.validateItems(this.destinationSelectedFurni); + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, this.destinationFurniSource, this.destinationSelectedFurni)) { + if (item == null) continue; + + Integer referenceValue = this.referenceFor(references, item.getId(), TARGET_FURNI, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + int currentValue = room.getFurniVariableManager().getCurrentValue(item.getId(), this.destinationVariableItemId); + room.getFurniVariableManager().updateVariableValue(item.getId(), this.destinationVariableItemId, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeFurniInternal(WiredContext ctx, Room room, String key) { + if (!canUseFurniInternalDestination(key)) return; + if (this.destinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) this.validateItems(this.destinationSelectedFurni); + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, this.destinationFurniSource, this.destinationSelectedFurni)) { + if (item == null) continue; + + Integer currentValue = this.readFurniInternalValue(room, item, key); + if (currentValue == null) continue; + + Integer referenceValue = this.referenceFor(references, item.getId(), TARGET_FURNI, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + this.writeFurniInternalValue(room, item, key, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeRoom(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) return; + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + Integer referenceValue = this.referenceFor(references, room.getId(), TARGET_ROOM, 0); + if (!this.isUnaryOperation() && referenceValue == null) return; + + int currentValue = room.getRoomVariableManager().getCurrentValue(this.destinationVariableItemId); + room.getRoomVariableManager().updateVariableValue(this.destinationVariableItemId, this.applyOperation(currentValue, referenceValue)); + } + + private void executeContext(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) return; + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + Integer referenceValue = this.referenceFor(references, this.destinationVariableItemId, TARGET_CONTEXT, 0); + if (!this.isUnaryOperation() && referenceValue == null) return; + if (!WiredContextVariableSupport.hasVariable(ctx, this.destinationVariableItemId)) return; + + Integer currentValue = WiredContextVariableSupport.getCurrentValue(ctx, this.destinationVariableItemId); + int nextValue = this.applyOperation(currentValue != null ? currentValue : 0, referenceValue); + WiredContextVariableSupport.updateVariableValue(ctx, room, this.destinationVariableItemId, nextValue); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.validateItems(this.destinationSelectedFurni); + this.validateItems(this.referenceSelectedFurni); + + List selectedItems = new ArrayList<>(); + if (this.destinationTargetType == TARGET_FURNI && this.destinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) selectedItems.addAll(this.destinationSelectedFurni); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + for (HabboItem item : selectedItems) message.appendInt(item.getId()); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(9); + message.appendInt(this.destinationTargetType); + message.appendInt(this.operation); + message.appendInt(this.referenceMode); + message.appendInt(this.referenceConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.destinationUserSource); + message.appendInt(this.destinationFurniSource); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = this.getRoom(); + if (room == null) throw new WiredSaveException("Room not found"); + + int[] params = settings.getIntParams(); + String[] stringParts = this.parseStringData(settings.getStringParam()); + int nextDestinationTargetType = normalizeTargetType(param(params, 0, TARGET_USER)); + int nextOperation = normalizeOperation(param(params, 1, OP_ASSIGN)); + int nextReferenceMode = normalizeReferenceMode(param(params, 2, REF_CONSTANT)); + int nextReferenceConstantValue = param(params, 3, 0); + int nextReferenceTargetType = normalizeTargetType(param(params, 4, TARGET_USER)); + int nextDestinationUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + int nextDestinationFurniSource = normalizeDestinationFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 7, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 8, WiredSourceUtil.SOURCE_TRIGGER)); + String nextDestinationVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : ""); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + + this.validateDestination(room, nextDestinationTargetType, nextDestinationVariableToken); + if (nextReferenceMode == REF_VARIABLE) this.validateReference(room, nextReferenceTargetType, nextReferenceVariableToken); + + int maxDelay = Emulator.getConfig().getInt("hotel.wired.max_delay", 20); + if (settings.getDelay() > maxDelay) throw new WiredSaveException("Delay too long"); + + List nextDestinationItems = (nextDestinationTargetType == TARGET_FURNI && nextDestinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) ? this.parseItems(settings.getFurniIds(), room) : new ArrayList<>(); + List nextReferenceItems = (nextReferenceMode == REF_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) ? this.parseItems((stringParts.length > 2) ? stringParts[2] : "", room) : new ArrayList<>(); + int selectionLimit = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); + if (nextDestinationItems.size() > selectionLimit || nextReferenceItems.size() > selectionLimit) throw new WiredSaveException("Too many furni selected"); + + this.destinationSelectedFurni.clear(); + this.destinationSelectedFurni.addAll(nextDestinationItems); + this.referenceSelectedFurni.clear(); + this.referenceSelectedFurni.addAll(nextReferenceItems); + this.destinationTargetType = nextDestinationTargetType; + this.setDestinationVariableToken(nextDestinationVariableToken); + this.operation = nextOperation; + this.referenceMode = nextReferenceMode; + this.referenceConstantValue = nextReferenceConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.setReferenceVariableToken(nextReferenceVariableToken); + this.destinationUserSource = nextDestinationUserSource; + this.destinationFurniSource = nextDestinationFurniSource; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.destinationTargetType, this.destinationVariableToken, this.destinationVariableItemId, this.operation, this.referenceMode, this.referenceConstantValue, this.referenceTargetType, this.referenceVariableToken, this.referenceVariableItemId, this.destinationUserSource, this.destinationFurniSource, this.referenceUserSource, this.referenceFurniSource, this.getDelay(), this.toIds(this.destinationSelectedFurni), this.toIds(this.referenceSelectedFurni))); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.destinationTargetType = normalizeTargetType(data.destinationTargetType); + this.setDestinationVariableToken(normalizeVariableToken((data.destinationVariableToken != null) ? data.destinationVariableToken : ((data.destinationVariableItemId > 0) ? String.valueOf(data.destinationVariableItemId) : ""))); + this.operation = normalizeOperation(data.operation); + this.referenceMode = normalizeReferenceMode(data.referenceMode); + this.referenceConstantValue = data.referenceConstantValue; + this.referenceTargetType = normalizeTargetType(data.referenceTargetType); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + this.destinationUserSource = normalizeUserSource(data.destinationUserSource); + this.destinationFurniSource = normalizeDestinationFurniSource(data.destinationFurniSource); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.setDelay(Math.max(0, data.delay)); + + if (room != null) { + try { + this.destinationSelectedFurni.addAll(this.parseItems(data.destinationSelectedFurniIds, room)); + this.referenceSelectedFurni.addAll(this.parseItems(data.referenceSelectedFurniIds, room)); + } catch (WiredSaveException ignored) { + } + } + } + + @Override + public void onPickUp() { + this.destinationTargetType = TARGET_USER; + this.setDestinationVariableToken(""); + this.operation = OP_ASSIGN; + this.referenceMode = REF_CONSTANT; + this.referenceConstantValue = 0; + this.referenceTargetType = TARGET_USER; + this.setReferenceVariableToken(""); + this.destinationUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.destinationFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.destinationSelectedFurni.clear(); + this.referenceSelectedFurni.clear(); + this.setDelay(0); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean requiresTriggeringUser() { + return (this.destinationTargetType == TARGET_USER && this.destinationUserSource == WiredSourceUtil.SOURCE_TRIGGER) + || (this.referenceMode == REF_VARIABLE && this.referenceTargetType == TARGET_USER && this.referenceUserSource == WiredSourceUtil.SOURCE_TRIGGER); + } + + private ReferenceSnapshot resolveReferences(WiredContext ctx, Room room) { + if (this.referenceMode != REF_VARIABLE) return null; + + return switch (this.referenceTargetType) { + case TARGET_USER -> this.userReferences(ctx, room); + case TARGET_FURNI -> this.furniReferences(ctx, room); + case TARGET_CONTEXT -> this.contextReferences(ctx, room); + case TARGET_ROOM -> this.roomReferences(room); + default -> null; + }; + } + + private ReferenceSnapshot userReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_USER); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseUserInternalReference(key)) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Integer value = this.readUserInternalValue(room, roomUnit, key); + if (value != null) snapshot.add(roomUnit.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) snapshot.add(roomUnit.getId(), room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot furniReferences(WiredContext ctx, Room room) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.validateItems(this.referenceSelectedFurni); + + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_FURNI); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseFurniInternalReference(key)) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedFurni)) { + if (item == null) continue; + + Integer value = this.readFurniInternalValue(room, item, key); + if (value != null) snapshot.add(item.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedFurni)) { + if (item != null) snapshot.add(item.getId(), room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot roomReferences(Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_ROOM); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseRoomInternalReference(key)) return null; + + Integer value = this.readRoomInternalValue(room, key); + if (value == null) return null; + + snapshot.add(room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + snapshot.add(room.getId(), room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId)); + return snapshot; + } + + private ReferenceSnapshot contextReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_CONTEXT); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseContextInternalReference(key)) return null; + + Integer value = WiredInternalVariableSupport.readContextValue(ctx, key); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId > 0 ? this.referenceVariableItemId : room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId, value); + return snapshot; + } + + private Integer referenceFor(ReferenceSnapshot snapshot, int destinationEntityId, int destinationTarget, int destinationIndex) { + if (this.referenceMode != REF_VARIABLE) return this.referenceConstantValue; + if (this.isUnaryOperation()) return 0; + if (snapshot == null || snapshot.isEmpty()) return null; + if (snapshot.targetType == destinationTarget && snapshot.values.containsKey(destinationEntityId)) return snapshot.values.get(destinationEntityId); + if (destinationIndex >= 0 && destinationIndex < snapshot.values.size()) return new ArrayList<>(snapshot.values.values()).get(destinationIndex); + return new ArrayList<>(snapshot.values.values()).get(0); + } + + private int applyOperation(int currentValue, Integer referenceValue) { + return switch (this.operation) { + case OP_ASSIGN -> (referenceValue != null) ? referenceValue : currentValue; + case OP_ADD -> clamp((long) currentValue + referenceValue); + case OP_SUB -> clamp((long) currentValue - referenceValue); + case OP_MUL -> clamp((long) currentValue * referenceValue); + case OP_DIV -> (referenceValue == null || referenceValue == 0) ? currentValue : (currentValue / referenceValue); + case OP_POW -> (referenceValue == null || referenceValue < 0) ? 0 : clamp(Math.round(Math.pow(currentValue, referenceValue))); + case OP_MOD -> (referenceValue == null || referenceValue == 0) ? currentValue : (currentValue % referenceValue); + case OP_MIN -> (referenceValue != null) ? Math.min(currentValue, referenceValue) : currentValue; + case OP_MAX -> (referenceValue != null) ? Math.max(currentValue, referenceValue) : currentValue; + case OP_RANDOM -> (referenceValue == null || referenceValue <= 0) ? 0 : Emulator.getRandom().nextInt(referenceValue + 1); + case OP_ABS -> (currentValue == Integer.MIN_VALUE) ? Integer.MAX_VALUE : Math.abs(currentValue); + case OP_AND -> (referenceValue != null) ? (currentValue & referenceValue) : currentValue; + case OP_OR -> (referenceValue != null) ? (currentValue | referenceValue) : currentValue; + case OP_XOR -> (referenceValue != null) ? (currentValue ^ referenceValue) : currentValue; + case OP_NOT -> ~currentValue; + case OP_LSHIFT -> currentValue << shift(referenceValue); + case OP_RSHIFT -> currentValue >> shift(referenceValue); + default -> currentValue; + }; + } + + private boolean isUnaryOperation() { + return this.operation == OP_ABS || this.operation == OP_NOT; + } + + private void validateDestination(Room room, int targetType, String variableToken) throws WiredSaveException { + if (variableToken == null || variableToken.isEmpty()) throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + + boolean valid = switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalDestination(getInternalVariableKey(variableToken)) + : this.isValidUserCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalDestination(getInternalVariableKey(variableToken)) + : this.isValidFurniCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> !isInternalVariableToken(variableToken) + && this.isValidContextCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> !isInternalVariableToken(variableToken) && this.isValidRoomCustomDestination(room, getCustomItemId(variableToken)); + default -> false; + }; + + if (!valid) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + private void validateReference(Room room, int targetType, String variableToken) throws WiredSaveException { + if (variableToken == null || variableToken.isEmpty()) throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + + boolean valid = switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalReference(getInternalVariableKey(variableToken)) + : this.isValidUserCustomReference(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalReference(getInternalVariableKey(variableToken)) + : this.isValidFurniCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> isInternalVariableToken(variableToken) + ? canUseContextInternalReference(getInternalVariableKey(variableToken)) + : this.isValidContextCustomReference(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> isInternalVariableToken(variableToken) + ? canUseRoomInternalReference(getInternalVariableKey(variableToken)) + : this.isValidRoomCustomReference(room, getCustomItemId(variableToken)); + default -> false; + }; + + if (!valid) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + private boolean isValidUserCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidFurniCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidRoomCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidContextCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidUserCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidRoomCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidContextCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + + private boolean isValidFurniCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private boolean writeUserInternalValue(Room room, RoomUnit roomUnit, String key, int value) { + return WiredInternalVariableSupport.writeUserValue(room, roomUnit, key, value); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private boolean writeFurniInternalValue(Room room, HabboItem item, String key, int value) { + return WiredInternalVariableSupport.writeFurniValue(room, item, key, value); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private Integer getTeamColorId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private Integer getTeamTypeId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + private boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y) { + if (room == null || roomUnit == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); + return WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), 0, true); + } + + private boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { + if (room == null || item == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + FurnitureMovementError error = room.moveFurniTo(item, targetTile, rotation, z, null, true, true); + return error == FurnitureMovementError.NONE; + } + + private List parseItems(int[] ids, Room room) throws WiredSaveException { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (int id : ids) { + HabboItem item = room.getHabboItem(id); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", id)); + items.add(item); + } + + return items; + } + + private List parseItems(List ids, Room room) throws WiredSaveException { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (Integer id : ids) { + if (id == null || id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", id)); + items.add(item); + } + + return items; + } + + private List parseItems(String ids, Room room) throws WiredSaveException { + List items = new ArrayList<>(); + if (ids == null || ids.trim().isEmpty() || room == null) return items; + + for (String part : ids.split("[;,\\t]")) { + int id = parseInteger(part); + if (id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", id)); + items.add(item); + } + + return items; + } + + private String serializeStringData() { + return (this.destinationVariableToken == null ? "" : this.destinationVariableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken) + DELIM + this.serializeIds(this.referenceSelectedFurni); + } + + private String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private List toIds(List items) { + List ids = new ArrayList<>(); + for (HabboItem item : items) if (item != null) ids.add(item.getId()); + return ids; + } + + private String serializeIds(List items) { + StringBuilder builder = new StringBuilder(); + + for (HabboItem item : items) { + if (item == null) continue; + if (builder.length() > 0) builder.append(FURNI_DELIM); + builder.append(item.getId()); + } + + return builder.toString(); + } + + private void setDestinationVariableToken(String token) { + this.destinationVariableToken = normalizeVariableToken(token); + this.destinationVariableItemId = getCustomItemId(this.destinationVariableToken); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + private static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + private static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + return parseInteger(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } + + private static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + private static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (isCustomVariableToken(normalized)) return normalized; + if (isInternalVariableToken(normalized)) return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + + int parsedValue = parseInteger(normalized); + return parsedValue > 0 ? CUSTOM_TOKEN_PREFIX + parsedValue : ""; + } + + private static boolean canUseUserInternalDestination(String key) { + return WiredInternalVariableSupport.canUseUserDestination(key); + } + + private static boolean canUseFurniInternalDestination(String key) { + return WiredInternalVariableSupport.canUseFurniDestination(key); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params.length > index) ? params[index] : fallback; + } + + private static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeReferenceMode(int value) { + return (value == REF_VARIABLE) ? REF_VARIABLE : REF_CONSTANT; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeDestinationFurniSource(int value) { + return switch (value) { + case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeOperation(int value) { + return switch (value) { + case OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_POW, OP_MOD, OP_MIN, OP_MAX, OP_RANDOM, OP_ABS, OP_AND, OP_OR, OP_XOR, OP_NOT, OP_LSHIFT, OP_RSHIFT -> value; + default -> OP_ASSIGN; + }; + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private static int shift(Integer value) { + return (value == null) ? 0 : Math.max(0, Math.min(31, value)); + } + + private static int clamp(long value) { + return (value > Integer.MAX_VALUE) ? Integer.MAX_VALUE : ((value < Integer.MIN_VALUE) ? Integer.MIN_VALUE : (int) value); + } + + static class JsonData { + int destinationTargetType, destinationVariableItemId, operation, referenceMode, referenceConstantValue, referenceTargetType, referenceVariableItemId, destinationUserSource, destinationFurniSource, referenceUserSource, referenceFurniSource, delay; + String destinationVariableToken, referenceVariableToken; + List destinationSelectedFurniIds, referenceSelectedFurniIds; + + JsonData(int destinationTargetType, String destinationVariableToken, int destinationVariableItemId, int operation, int referenceMode, int referenceConstantValue, int referenceTargetType, String referenceVariableToken, int referenceVariableItemId, int destinationUserSource, int destinationFurniSource, int referenceUserSource, int referenceFurniSource, int delay, List destinationSelectedFurniIds, List referenceSelectedFurniIds) { + this.destinationTargetType = destinationTargetType; + this.destinationVariableToken = destinationVariableToken; + this.destinationVariableItemId = destinationVariableItemId; + this.operation = operation; + this.referenceMode = referenceMode; + this.referenceConstantValue = referenceConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.destinationUserSource = destinationUserSource; + this.destinationFurniSource = destinationFurniSource; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.delay = delay; + this.destinationSelectedFurniIds = destinationSelectedFurniIds; + this.referenceSelectedFurniIds = referenceSelectedFurniIds; + } + } + + private static class ReferenceSnapshot { + final int targetType; + final LinkedHashMap values = new LinkedHashMap<>(); + + ReferenceSnapshot(int targetType) { + this.targetType = targetType; + } + + void add(int entityId, int value) { + this.values.put(entityId, value); + } + + boolean isEmpty() { + return this.values.isEmpty(); + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveVariable.java new file mode 100644 index 00000000..73c26bba --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveVariable.java @@ -0,0 +1,434 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredEffectGiveVariable extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.GIVE_VAR; + + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_CONTEXT = 2; + + private int variableItemId = 0; + private int targetType = TARGET_USER; + private boolean overrideExisting = false; + private int initialValue = 0; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private final THashSet selectedFurni; + + public WiredEffectGiveVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.selectedFurni = new THashSet<>(); + } + + public WiredEffectGiveVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.selectedFurni = new THashSet<>(); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null) { + return; + } + + switch (this.targetType) { + case TARGET_USER: + this.executeUserVariables(ctx, room); + return; + case TARGET_FURNI: + this.executeFurniVariables(ctx, room); + return; + case TARGET_CONTEXT: + this.executeContextVariables(ctx, room); + return; + default: + return; + } + } + + private void executeContextVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return; + } + + Integer value = definitionInfo.hasValue() ? this.initialValue : null; + WiredContextVariableSupport.assignVariable(ctx, room, this.variableItemId, value, this.overrideExisting); + } + + private void executeUserVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definitionInfo = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return; + } + + List users = WiredSourceUtil.resolveUsers(ctx, this.userSource); + + if (users.isEmpty()) { + return; + } + + Integer value = definitionInfo.hasValue() ? this.initialValue : null; + + for (RoomUnit roomUnit : users) { + if (roomUnit == null) { + continue; + } + + Habbo habbo = room.getHabbo(roomUnit); + + if (habbo == null) { + continue; + } + + room.getUserVariableManager().assignVariable(habbo, this.variableItemId, value, this.overrideExisting); + } + } + + private void executeFurniVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.validateItems(this.selectedFurni); + } + + List furni = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedFurni); + + if (furni.isEmpty()) { + return; + } + + Integer value = definition.hasValue() ? this.initialValue : null; + + for (HabboItem item : furni) { + if (item == null) { + continue; + } + + room.getFurniVariableManager().assignVariable(item, this.variableItemId, value, this.overrideExisting); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List selectedItems = new ArrayList<>(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (HabboItem item : this.selectedFurni) { + if (item != null && room != null && room.getHabboItem(item.getId()) != null) { + selectedItems.add(item); + } + } + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + + for (HabboItem item : selectedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(String.valueOf(this.variableItemId)); + message.appendInt(5); + message.appendInt(this.targetType); + message.appendInt(this.overrideExisting ? 1 : 0); + message.appendInt(this.initialValue); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + int nextTargetType = normalizeTargetType((intParams.length > 0) ? intParams[0] : TARGET_USER); + boolean nextOverrideExisting = (intParams.length > 1) && (intParams[1] == 1); + int nextInitialValue = (intParams.length > 2) ? intParams[2] : 0; + int nextUserSource = normalizeUserSource((intParams.length > 3) ? intParams[3] : WiredSourceUtil.SOURCE_TRIGGER); + int nextFurniSource = normalizeFurniSource((intParams.length > 4) ? intParams[4] : WiredSourceUtil.SOURCE_TRIGGER); + int nextVariableItemId = parseVariableItemId(settings.getStringParam()); + + if (nextVariableItemId <= 0 && settings.getFurniIds() != null && settings.getFurniIds().length > 0) { + int legacyItemId = settings.getFurniIds()[0]; + + if (room.getUserVariableManager().hasDefinition(legacyItemId)) { + nextVariableItemId = legacyItemId; + nextTargetType = TARGET_USER; + } + } + + if (nextVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + WiredVariableDefinitionInfo userDefinitionInfo = (nextTargetType == TARGET_USER) ? room.getUserVariableManager().getDefinitionInfo(nextVariableItemId) : null; + + if (nextTargetType == TARGET_USER && (userDefinitionInfo == null || userDefinitionInfo.isReadOnly())) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (nextTargetType == TARGET_FURNI) { + WiredVariableDefinitionInfo furniDefinitionInfo = room.getFurniVariableManager().getDefinitionInfo(nextVariableItemId); + + if (furniDefinitionInfo == null || furniDefinitionInfo.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + } + + if (nextTargetType == TARGET_CONTEXT) { + WiredVariableDefinitionInfo contextDefinitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, nextVariableItemId); + + if (contextDefinitionInfo == null || contextDefinitionInfo.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + } + + this.selectedFurni.clear(); + + if (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) { + int[] furniIds = settings.getFurniIds(); + int itemsCount = (furniIds != null) ? furniIds.length : 0; + + if (itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + for (int i = 0; i < itemsCount; i++) { + int itemId = furniIds[i]; + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + this.selectedFurni.add(item); + } + } + + this.variableItemId = nextVariableItemId; + this.targetType = nextTargetType; + this.overrideExisting = nextOverrideExisting; + this.initialValue = nextInitialValue; + this.userSource = nextUserSource; + this.furniSource = nextFurniSource; + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public String getWiredData() { + List selectedItemIds = new ArrayList<>(); + + for (HabboItem item : this.selectedFurni) { + if (item != null) { + selectedItemIds.add(item.getId()); + } + } + + return WiredManager.getGson().toJson(new JsonData(this.variableItemId, this.targetType, this.overrideExisting, this.initialValue, this.userSource, this.furniSource, this.getDelay(), selectedItemIds)); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableItemId = Math.max(0, data.variableItemId); + this.targetType = normalizeTargetType(data.targetType); + this.overrideExisting = data.overrideExisting; + this.initialValue = data.initialValue; + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.setDelay(Math.max(0, data.delay)); + + if (room != null && data.selectedFurniIds != null) { + for (Integer itemId : data.selectedFurniIds) { + if (itemId == null || itemId <= 0) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.selectedFurni.add(item); + } + } + } + } + + return; + } + + try { + this.variableItemId = Math.max(0, Integer.parseInt(wiredData.trim())); + this.targetType = TARGET_USER; + } catch (NumberFormatException ignored) { + } + } + + @Override + public void onPickUp() { + this.variableItemId = 0; + this.targetType = TARGET_USER; + this.overrideExisting = false; + this.initialValue = 0; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.selectedFurni.clear(); + this.setDelay(0); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public WiredEffectType getType() { + return type; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public int getTargetType() { + return this.targetType; + } + + public boolean isOverrideExisting() { + return this.overrideExisting; + } + + public int getInitialValue() { + return this.initialValue; + } + + public int getUserSource() { + return this.userSource; + } + + public int getFurniSource() { + return this.furniSource; + } + + public THashSet getSelectedFurni() { + return this.selectedFurni; + } + + private static int normalizeTargetType(int value) { + switch (value) { + case TARGET_FURNI: + case TARGET_CONTEXT: + return value; + default: + return TARGET_USER; + } + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private static int parseVariableItemId(String value) { + if (value == null || value.trim().isEmpty()) { + return 0; + } + + try { + return Math.max(0, Integer.parseInt(value.trim())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + int variableItemId; + int targetType; + boolean overrideExisting; + int initialValue; + int userSource; + int furniSource; + int delay; + List selectedFurniIds; + + JsonData(int variableItemId, int targetType, boolean overrideExisting, int initialValue, int userSource, int furniSource, int delay, List selectedFurniIds) { + this.variableItemId = variableItemId; + this.targetType = targetType; + this.overrideExisting = overrideExisting; + this.initialValue = initialValue; + this.userSource = userSource; + this.furniSource = furniSource; + this.delay = delay; + this.selectedFurniIds = selectedFurniIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java new file mode 100644 index 00000000..d2812e61 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java @@ -0,0 +1,370 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredEffectRemoveVariable extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.REMOVE_VAR; + + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_CONTEXT = 2; + + private int variableItemId = 0; + private int targetType = TARGET_USER; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private final THashSet selectedFurni; + + public WiredEffectRemoveVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.selectedFurni = new THashSet<>(); + } + + public WiredEffectRemoveVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.selectedFurni = new THashSet<>(); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null) { + return; + } + + switch (this.targetType) { + case TARGET_USER: + this.executeUserVariables(ctx, room); + return; + case TARGET_FURNI: + this.executeFurniVariables(ctx, room); + return; + case TARGET_CONTEXT: + this.executeContextVariables(ctx, room); + return; + default: + return; + } + } + + private void executeUserVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + List users = WiredSourceUtil.resolveUsers(ctx, this.userSource); + + for (RoomUnit roomUnit : users) { + if (roomUnit == null) { + continue; + } + + Habbo habbo = room.getHabbo(roomUnit); + + if (habbo == null) { + continue; + } + + room.getUserVariableManager().removeVariable(habbo.getHabboInfo().getId(), this.variableItemId); + } + } + + private void executeFurniVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.validateItems(this.selectedFurni); + } + + List furni = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedFurni); + + for (HabboItem item : furni) { + if (item == null) { + continue; + } + + room.getFurniVariableManager().removeVariable(item.getId(), this.variableItemId); + } + } + + private void executeContextVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + WiredContextVariableSupport.removeVariable(ctx, room, this.variableItemId); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List selectedItems = new ArrayList<>(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (HabboItem item : this.selectedFurni) { + if (item != null && room != null && room.getHabboItem(item.getId()) != null) { + selectedItems.add(item); + } + } + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + + for (HabboItem item : selectedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(String.valueOf(this.variableItemId)); + message.appendInt(3); + message.appendInt(this.targetType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + int nextTargetType = normalizeTargetType((intParams.length > 0) ? intParams[0] : TARGET_USER); + int nextUserSource = normalizeUserSource((intParams.length > 1) ? intParams[1] : WiredSourceUtil.SOURCE_TRIGGER); + int nextFurniSource = normalizeFurniSource((intParams.length > 2) ? intParams[2] : WiredSourceUtil.SOURCE_TRIGGER); + int nextVariableItemId = parseVariableItemId(settings.getStringParam()); + + if (nextVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + switch (nextTargetType) { + case TARGET_USER: + WiredVariableDefinitionInfo userDefinition = room.getUserVariableManager().getDefinitionInfo(nextVariableItemId); + if (userDefinition == null || userDefinition.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + break; + case TARGET_FURNI: + WiredVariableDefinitionInfo furniDefinition = room.getFurniVariableManager().getDefinitionInfo(nextVariableItemId); + if (furniDefinition == null || furniDefinition.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + break; + case TARGET_CONTEXT: + WiredVariableDefinitionInfo contextDefinition = WiredContextVariableSupport.getDefinitionInfo(room, nextVariableItemId); + if (contextDefinition == null || contextDefinition.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + break; + default: + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + this.selectedFurni.clear(); + + if (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) { + int[] furniIds = settings.getFurniIds(); + int itemsCount = (furniIds != null) ? furniIds.length : 0; + + if (itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + for (int i = 0; i < itemsCount; i++) { + int itemId = furniIds[i]; + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + this.selectedFurni.add(item); + } + } + + this.variableItemId = nextVariableItemId; + this.targetType = nextTargetType; + this.userSource = nextUserSource; + this.furniSource = nextFurniSource; + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public String getWiredData() { + List selectedItemIds = new ArrayList<>(); + + for (HabboItem item : this.selectedFurni) { + if (item != null) { + selectedItemIds.add(item.getId()); + } + } + + return WiredManager.getGson().toJson(new JsonData(this.variableItemId, this.targetType, this.userSource, this.furniSource, this.getDelay(), selectedItemIds)); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableItemId = Math.max(0, data.variableItemId); + this.targetType = normalizeTargetType(data.targetType); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.setDelay(Math.max(0, data.delay)); + + if (room != null && data.selectedFurniIds != null) { + for (Integer itemId : data.selectedFurniIds) { + if (itemId == null || itemId <= 0) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.selectedFurni.add(item); + } + } + } + } + + return; + } + + try { + this.variableItemId = Math.max(0, Integer.parseInt(wiredData.trim())); + this.targetType = TARGET_USER; + } catch (NumberFormatException ignored) { + } + } + + @Override + public void onPickUp() { + this.variableItemId = 0; + this.targetType = TARGET_USER; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.selectedFurni.clear(); + this.setDelay(0); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public WiredEffectType getType() { + return type; + } + + private static int normalizeTargetType(int value) { + switch (value) { + case TARGET_FURNI: + case TARGET_CONTEXT: + return value; + default: + return TARGET_USER; + } + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private static int parseVariableItemId(String value) { + if (value == null || value.trim().isEmpty()) { + return 0; + } + + try { + return Math.max(0, Integer.parseInt(value.trim())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + int variableItemId; + int targetType; + int userSource; + int furniSource; + int delay; + List selectedFurniIds; + + JsonData(int variableItemId, int targetType, int userSource, int furniSource, int delay, List selectedFurniIds) { + this.variableItemId = variableItemId; + this.targetType = targetType; + this.userSource = userSource; + this.furniSource = furniSource; + this.delay = delay; + this.selectedFurniIds = selectedFurniIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index 82bdd99c..fd2b5a45 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -122,13 +122,13 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { for (RoomUnit user : usersToSend) { for (HabboItem sourceItem : furniToSend) { for (HabboItem antenna : resolvedAntennas) { - fireSignalAtAntenna(room, antenna, user, sourceItem, nextDepth); + fireSignalAtAntenna(ctx, room, antenna, user, sourceItem, nextDepth); } } } } - private void fireSignalAtAntenna(Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth) { + private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth) { if (antenna == null) return; RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); if (tile == null) return; @@ -144,6 +144,9 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .tile(tile) .callStackDepth(depth) .signalChannel(signalChannel) + .signalUserCount(actor != null ? 1 : 0) + .signalFurniCount(sourceItem != null ? 1 : 0) + .contextVariableScope(ctx.contextVariables()) .triggeredByEffect(true); if (actor != null) builder.actor(actor); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraContextVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraContextVariable.java new file mode 100644 index 00000000..ce0ee961 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraContextVariable.java @@ -0,0 +1,132 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraContextVariable extends InteractionWiredExtra { + public static final int CODE = 84; + + private String variableName = ""; + private boolean hasValue = false; + + public WiredExtraContextVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraContextVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.hasValue = (intParams.length > 0) && (intParams[0] == 1); + + WiredContextVariableSupport.broadcastDefinitions(room); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.hasValue)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(1); + message.appendInt(this.hasValue ? 1 : 0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.hasValue = data.hasValue; + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.hasValue = false; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public boolean hasValue() { + return this.hasValue; + } + + static class JsonData { + String variableName; + boolean hasValue; + + JsonData(String variableName, boolean hasValue) { + this.variableName = variableName; + this.hasValue = hasValue; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurniByVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurniByVariable.java new file mode 100644 index 00000000..153294c6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurniByVariable.java @@ -0,0 +1,28 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFilterFurniByVariable extends WiredExtraVariableFilterBase { + public static final int CODE = 78; + + public WiredExtraFilterFurniByVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFilterFurniByVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_FURNI; + } + + @Override + protected int getCode() { + return CODE; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUsersByVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUsersByVariable.java new file mode 100644 index 00000000..660627ee --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUsersByVariable.java @@ -0,0 +1,28 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFilterUsersByVariable extends WiredExtraVariableFilterBase { + public static final int CODE = 77; + + public WiredExtraFilterUsersByVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFilterUsersByVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_USER; + } + + @Override + protected int getCode() { + return CODE; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFurniVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFurniVariable.java new file mode 100644 index 00000000..7a2dcd7d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFurniVariable.java @@ -0,0 +1,157 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFurniVariable extends InteractionWiredExtra { + public static final int CODE = 71; + public static final int AVAILABILITY_ROOM_ACTIVE = 1; + public static final int AVAILABILITY_PERMANENT = 10; + + private String variableName = ""; + private boolean hasValue = false; + private int availability = AVAILABILITY_ROOM_ACTIVE; + + public WiredExtraFurniVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFurniVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] intParams = settings.getIntParams(); + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.hasValue = (intParams.length > 0) && (intParams[0] == 1); + this.availability = normalizeAvailability((intParams.length > 1) ? intParams[1] : AVAILABILITY_ROOM_ACTIVE); + + room.getFurniVariableManager().handleDefinitionUpdated(this); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.hasValue, this.availability)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(2); + message.appendInt(this.hasValue ? 1 : 0); + message.appendInt(this.availability); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.hasValue = data.hasValue; + this.availability = normalizeAvailability(data.availability); + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.hasValue = false; + this.availability = AVAILABILITY_ROOM_ACTIVE; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isPermanentAvailability() { + return this.availability == AVAILABILITY_PERMANENT; + } + + private static int normalizeAvailability(int value) { + if (value == AVAILABILITY_PERMANENT) { + return AVAILABILITY_PERMANENT; + } + + return AVAILABILITY_ROOM_ACTIVE; + } + + static class JsonData { + String variableName; + boolean hasValue; + int availability; + + JsonData(String variableName, boolean hasValue, int availability) { + this.variableName = variableName; + this.hasValue = hasValue; + this.availability = availability; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRoomVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRoomVariable.java new file mode 100644 index 00000000..3481fc67 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRoomVariable.java @@ -0,0 +1,159 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraRoomVariable extends InteractionWiredExtra { + public static final int CODE = 72; + public static final int AVAILABILITY_ROOM_ACTIVE = 1; + public static final int AVAILABILITY_PERMANENT = 10; + public static final int AVAILABILITY_SHARED = 11; + + private String variableName = ""; + private int availability = AVAILABILITY_ROOM_ACTIVE; + + public WiredExtraRoomVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraRoomVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + int[] intParams = settings.getIntParams(); + + this.variableName = normalizedName; + this.availability = normalizeAvailability((intParams.length > 0) ? intParams[0] : AVAILABILITY_ROOM_ACTIVE); + + room.getRoomVariableManager().handleDefinitionUpdated(this); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.availability)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + int currentValue = (room != null) ? room.getRoomVariableManager().getCurrentValue(this.getId()) : 0; + + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(2); + message.appendInt(this.availability); + message.appendInt(currentValue); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.availability = normalizeAvailability(data.availability); + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.availability = AVAILABILITY_ROOM_ACTIVE; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public boolean hasValue() { + return true; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isPermanentAvailability() { + return this.availability == AVAILABILITY_PERMANENT || this.availability == AVAILABILITY_SHARED; + } + + public boolean isSharedAvailability() { + return this.availability == AVAILABILITY_SHARED; + } + + private static int normalizeAvailability(int value) { + if (value == AVAILABILITY_PERMANENT || value == AVAILABILITY_SHARED) { + return value; + } + + return AVAILABILITY_ROOM_ACTIVE; + } + + static class JsonData { + String variableName; + int availability; + + JsonData(String variableName, int availability) { + this.variableName = variableName; + this.availability = availability; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java new file mode 100644 index 00000000..504abe1c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java @@ -0,0 +1,271 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.regex.Pattern; + +public class WiredExtraTextInputVariable extends InteractionWiredExtra { + public static final int CODE = 85; + public static final int DISPLAY_NUMERIC = 1; + public static final int DISPLAY_TEXTUAL = 2; + public static final String DEFAULT_CAPTURER_NAME = ""; + public static final int MAX_CAPTURER_NAME_LENGTH = 32; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final Pattern WRAPPED_PLACEHOLDER_PATTERN = Pattern.compile("^#(.*)#$"); + + private int variableItemId = 0; + private String variableToken = ""; + private String capturerName = DEFAULT_CAPTURER_NAME; + private int displayType = DISPLAY_NUMERIC; + + public WiredExtraTextInputVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraTextInputVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + String[] stringData = splitStringData(settings.getStringParam()); + String nextVariableToken = normalizeVariableToken(stringData[0]); + int nextVariableItemId = getCustomItemId(nextVariableToken); + + if (nextVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + WiredVariableDefinitionInfo definitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, nextVariableItemId); + if (definitionInfo == null || !definitionInfo.hasValue()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + this.variableItemId = nextVariableItemId; + this.variableToken = nextVariableToken; + this.capturerName = normalizeCapturerName(stringData[1]); + this.displayType = normalizeDisplayType((intParams.length > 0) ? intParams[0] : DISPLAY_NUMERIC); + + if (!canUseTextualDisplay(room, this.variableItemId)) { + this.displayType = DISPLAY_NUMERIC; + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableToken, this.variableItemId, this.capturerName, this.displayType)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken + "\t" + this.capturerName); + message.appendInt(1); + message.appendInt(this.getDisplayType(room)); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableToken = normalizeVariableToken((data.variableToken != null) + ? data.variableToken + : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : "")); + this.variableItemId = getCustomItemId(this.variableToken); + this.capturerName = normalizeCapturerName(data.capturerName); + this.displayType = normalizeDisplayType(data.displayType); + } + + return; + } + + String[] legacyData = splitStringData(wiredData); + this.variableToken = normalizeVariableToken(legacyData[0]); + this.variableItemId = getCustomItemId(this.variableToken); + this.capturerName = normalizeCapturerName(legacyData[1]); + } + + @Override + public void onPickUp() { + this.variableItemId = 0; + this.variableToken = ""; + this.capturerName = DEFAULT_CAPTURER_NAME; + this.displayType = DISPLAY_NUMERIC; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public String getVariableToken() { + return this.variableToken; + } + + public String getCapturerName() { + return this.capturerName; + } + + public String getPlaceholderToken() { + return this.capturerName.isEmpty() ? "" : "#" + this.capturerName + "#"; + } + + public int getDisplayType(Room room) { + return (this.displayType == DISPLAY_TEXTUAL && canUseTextualDisplay(room, this.variableItemId)) + ? DISPLAY_TEXTUAL + : DISPLAY_NUMERIC; + } + + public Integer resolveCapturedValue(Room room, String rawValue) { + String normalizedValue = rawValue != null ? rawValue.trim() : ""; + if (normalizedValue.isEmpty()) { + return null; + } + + if (this.getDisplayType(room) == DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, normalizedValue); + } + + try { + return Integer.parseInt(normalizedValue); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static boolean canUseTextualDisplay(Room room, int definitionItemId) { + WiredVariableDefinitionInfo definitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, definitionItemId); + return definitionInfo != null && definitionInfo.hasValue() && definitionInfo.isTextConnected(); + } + + private static String[] splitStringData(String value) { + if (value == null) { + return new String[]{ "", DEFAULT_CAPTURER_NAME }; + } + + String[] parts = value.split("\t", -1); + if (parts.length == 1) { + return new String[]{ parts[0], DEFAULT_CAPTURER_NAME }; + } + + return new String[]{ parts[0], parts[1] }; + } + + private static int normalizeDisplayType(int value) { + return (value == DISPLAY_TEXTUAL) ? DISPLAY_TEXTUAL : DISPLAY_NUMERIC; + } + + private static String normalizeCapturerName(String value) { + if (value == null) { + return DEFAULT_CAPTURER_NAME; + } + + String normalized = value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + if (WRAPPED_PLACEHOLDER_PATTERN.matcher(normalized).matches()) { + normalized = normalized.substring(1, normalized.length() - 1).trim(); + } + + if (normalized.length() > MAX_CAPTURER_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_CAPTURER_NAME_LENGTH); + } + + return normalized; + } + + private static String normalizeVariableToken(String value) { + String normalized = value == null ? "" : value.trim(); + if (normalized.isEmpty()) { + return ""; + } + + if (normalized.startsWith(CUSTOM_TOKEN_PREFIX)) { + return normalized; + } + + try { + int parsedValue = Integer.parseInt(normalized); + return parsedValue > 0 ? (CUSTOM_TOKEN_PREFIX + parsedValue) : ""; + } catch (NumberFormatException ignored) { + return ""; + } + } + + private static int getCustomItemId(String token) { + if (token == null || !token.startsWith(CUSTOM_TOKEN_PREFIX)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + String variableToken; + int variableItemId; + String capturerName; + int displayType; + + JsonData(String variableToken, int variableItemId, String capturerName, int displayType) { + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.capturerName = capturerName; + this.displayType = displayType; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputVariable.java new file mode 100644 index 00000000..9e9c9beb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputVariable.java @@ -0,0 +1,544 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class WiredExtraTextOutputVariable extends InteractionWiredExtra { + public static final int CODE = 80; + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_CONTEXT = 2; + public static final int TARGET_ROOM = 3; + public static final int DISPLAY_NUMERIC = 1; + public static final int DISPLAY_TEXTUAL = 2; + public static final int TYPE_SINGLE = 1; + public static final int TYPE_MULTIPLE = 2; + public static final String DEFAULT_VARIABLE_TOKEN = ""; + public static final String DEFAULT_PLACEHOLDER_NAME = ""; + public static final String DEFAULT_DELIMITER = ", "; + public static final int MAX_PLACEHOLDER_NAME_LENGTH = 32; + public static final int MAX_DELIMITER_LENGTH = 16; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final Pattern WRAPPED_PLACEHOLDER_PATTERN = Pattern.compile("^\\$\\((.*)\\)$"); + + private final THashSet items; + private int targetType = TARGET_USER; + private int displayType = DISPLAY_NUMERIC; + private int placeholderType = TYPE_SINGLE; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int variableItemId = 0; + private String variableToken = DEFAULT_VARIABLE_TOKEN; + private String placeholderName = DEFAULT_PLACEHOLDER_NAME; + private String delimiter = DEFAULT_DELIMITER; + + public WiredExtraTextOutputVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.items = new THashSet<>(); + } + + public WiredExtraTextOutputVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + String[] stringData = splitStringData(settings.getStringParam()); + int nextTargetType = normalizeTargetType((intParams.length > 0) ? intParams[0] : TARGET_USER); + String nextVariableToken = normalizeVariableToken(stringData[0]); + + if (nextVariableToken.isEmpty()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + if (!isValidVariable(room, nextTargetType, nextVariableToken)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + int nextFurniSource = normalizeFurniSource((intParams.length > 4) ? intParams[4] : WiredSourceUtil.SOURCE_TRIGGER); + this.items.clear(); + + if (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) { + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + } + + this.targetType = nextTargetType; + this.setVariableToken(nextVariableToken); + this.displayType = normalizeDisplayType((intParams.length > 1) ? intParams[1] : DISPLAY_NUMERIC); + this.placeholderType = normalizePlaceholderType((intParams.length > 2) ? intParams[2] : TYPE_SINGLE); + this.userSource = normalizeUserSource((intParams.length > 3) ? intParams[3] : WiredSourceUtil.SOURCE_TRIGGER); + this.furniSource = nextFurniSource; + this.placeholderName = normalizePlaceholderName(stringData[1]); + this.delimiter = normalizeDelimiter(stringData[2]); + + if (!canUseTextualDisplay(room, this.targetType, this.variableToken)) { + this.displayType = DISPLAY_NUMERIC; + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.targetType, + this.variableToken, + this.variableItemId, + this.displayType, + this.placeholderType, + this.userSource, + this.furniSource, + this.placeholderName, + this.delimiter, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + List selectedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + selectedItems.addAll(this.items); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + + for (HabboItem item : selectedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken + "\t" + this.placeholderName + "\t" + this.delimiter); + message.appendInt(5); + message.appendInt(this.targetType); + message.appendInt(this.displayType); + message.appendInt(this.placeholderType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.targetType = normalizeTargetType(data.targetType); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.displayType = normalizeDisplayType(data.displayType); + this.placeholderType = normalizePlaceholderType(data.placeholderType); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.placeholderName = normalizePlaceholderName(data.placeholderName); + this.delimiter = normalizeDelimiter(data.delimiter); + + if (room != null && data.itemIds != null) { + for (Integer itemId : data.itemIds) { + if (itemId == null || itemId <= 0) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + if (item != null) { + this.items.add(item); + } + } + } + + if (room == null || !canUseTextualDisplay(room, this.targetType, this.variableToken)) { + this.displayType = DISPLAY_NUMERIC; + } + } + + return; + } + + String[] legacyData = splitStringData(wiredData); + this.setVariableToken(normalizeVariableToken(legacyData[0])); + this.placeholderName = normalizePlaceholderName(legacyData[1]); + this.delimiter = normalizeDelimiter(legacyData[2]); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.targetType = TARGET_USER; + this.setVariableToken(DEFAULT_VARIABLE_TOKEN); + this.displayType = DISPLAY_NUMERIC; + this.placeholderType = TYPE_SINGLE; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.placeholderName = DEFAULT_PLACEHOLDER_NAME; + this.delimiter = DEFAULT_DELIMITER; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getTargetType() { + return this.targetType; + } + + public String getVariableToken() { + return this.variableToken; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public int getDisplayType(Room room) { + return (this.displayType == DISPLAY_TEXTUAL && canUseTextualDisplay(room, this.targetType, this.variableToken)) + ? DISPLAY_TEXTUAL + : DISPLAY_NUMERIC; + } + + public int getPlaceholderType() { + return this.placeholderType; + } + + public String getPlaceholderName() { + return this.placeholderName; + } + + public String getPlaceholderToken() { + return this.placeholderName.isEmpty() ? "" : "$(" + this.placeholderName + ")"; + } + + public String getDelimiter() { + return this.delimiter; + } + + public int getUserSource() { + return this.userSource; + } + + public int getFurniSource() { + return this.furniSource; + } + + public THashSet getItems() { + return this.items; + } + + public boolean requiresActor() { + return this.targetType == TARGET_USER + && (this.userSource == WiredSourceUtil.SOURCE_TRIGGER || this.userSource == WiredSourceUtil.SOURCE_CLICKED_USER); + } + + public void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (room == null || room.getHabboItem(item.getId()) == null) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + public static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + public static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + public static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + public static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + private static String[] splitStringData(String value) { + if (value == null) { + return new String[]{ DEFAULT_VARIABLE_TOKEN, DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER }; + } + + String[] parts = value.split("\t", -1); + if (parts.length == 1) { + return new String[]{ value, DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER }; + } + + if (parts.length == 2) { + return new String[]{ parts[0], parts[1], DEFAULT_DELIMITER }; + } + + return new String[]{ parts[0], parts[1], parts[2] }; + } + + private static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeDisplayType(int value) { + return (value == DISPLAY_TEXTUAL) ? DISPLAY_TEXTUAL : DISPLAY_NUMERIC; + } + + private static int normalizePlaceholderType(int value) { + return (value == TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeFurniSource(int value) { + return switch (value) { + case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static String normalizePlaceholderName(String value) { + if (value == null) { + return DEFAULT_PLACEHOLDER_NAME; + } + + String normalized = value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + if (WRAPPED_PLACEHOLDER_PATTERN.matcher(normalized).matches()) { + normalized = normalized.substring(2, normalized.length() - 1).trim(); + } + + if (normalized.length() > MAX_PLACEHOLDER_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_PLACEHOLDER_NAME_LENGTH); + } + + return normalized; + } + + private static String normalizeDelimiter(String value) { + if (value == null) { + return DEFAULT_DELIMITER; + } + + String normalized = value.replace("\t", "").replace("\r", "").replace("\n", ""); + if (normalized.length() > MAX_DELIMITER_LENGTH) { + normalized = normalized.substring(0, MAX_DELIMITER_LENGTH); + } + + return normalized; + } + + private static String normalizeVariableToken(String value) { + String normalized = (value == null) ? "" : value.trim(); + if (normalized.isEmpty()) { + return ""; + } + + if (isCustomVariableToken(normalized)) { + return normalized; + } + + if (isInternalVariableToken(normalized)) { + return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + } + + try { + int parsedValue = Integer.parseInt(normalized); + return parsedValue > 0 ? (CUSTOM_TOKEN_PREFIX + parsedValue) : ""; + } catch (NumberFormatException ignored) { + return ""; + } + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private static boolean canUseTextualDisplay(Room room, int targetType, String variableToken) { + if (room == null || !isCustomVariableToken(variableToken)) { + return false; + } + + int itemId = getCustomItemId(variableToken); + if (itemId <= 0) { + return false; + } + + return switch (targetType) { + case TARGET_USER -> { + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + case TARGET_FURNI -> { + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + case TARGET_CONTEXT -> { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + default -> false; + }; + } + + private static boolean isValidVariable(Room room, int targetType, String variableToken) { + if (room == null) { + return false; + } + + return switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalReference(getInternalVariableKey(variableToken)) + : isUserCustomValue(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalReference(getInternalVariableKey(variableToken)) + : isFurniCustomValue(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> isInternalVariableToken(variableToken) + ? WiredInternalVariableSupport.canUseContextReference(getInternalVariableKey(variableToken)) + : isContextCustomValue(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> isInternalVariableToken(variableToken) + ? canUseRoomInternalReference(getInternalVariableKey(variableToken)) + : isRoomCustomValue(room, getCustomItemId(variableToken)); + default -> false; + }; + } + + private static boolean isUserCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(itemId) : null; + return definition != null && definition.hasValue(); + } + + private static boolean isFurniCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(itemId) : null; + return definition != null && definition.hasValue(); + } + + private static boolean isRoomCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(itemId) : null; + return definition != null && definition.hasValue(); + } + + private static boolean isContextCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, itemId); + return definition != null && definition.hasValue(); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + static class JsonData { + int targetType; + String variableToken; + int variableItemId; + int displayType; + int placeholderType; + int userSource; + int furniSource; + String placeholderName; + String delimiter; + List itemIds; + + JsonData(int targetType, String variableToken, int variableItemId, int displayType, int placeholderType, int userSource, int furniSource, String placeholderName, String delimiter, List itemIds) { + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.displayType = displayType; + this.placeholderType = placeholderType; + this.userSource = userSource; + this.furniSource = furniSource; + this.placeholderName = placeholderName; + this.delimiter = delimiter; + this.itemIds = itemIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUserVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUserVariable.java new file mode 100644 index 00000000..f02a8571 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUserVariable.java @@ -0,0 +1,162 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraUserVariable extends InteractionWiredExtra { + public static final int CODE = 70; + public static final int AVAILABILITY_ROOM = 0; + public static final int AVAILABILITY_PERMANENT = 10; + public static final int AVAILABILITY_SHARED = 11; + + private String variableName = ""; + private boolean hasValue = false; + private int availability = AVAILABILITY_ROOM; + + public WiredExtraUserVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraUserVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] intParams = settings.getIntParams(); + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.hasValue = (intParams.length > 0) && (intParams[0] == 1); + this.availability = normalizeAvailability((intParams.length > 1) ? intParams[1] : AVAILABILITY_ROOM); + + room.getUserVariableManager().handleDefinitionUpdated(this); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.hasValue, this.availability)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(2); + message.appendInt(this.hasValue ? 1 : 0); + message.appendInt(this.availability); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.hasValue = data.hasValue; + this.availability = normalizeAvailability(data.availability); + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.hasValue = false; + this.availability = AVAILABILITY_ROOM; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return variableName; + } + + public boolean hasValue() { + return hasValue; + } + + public int getAvailability() { + return availability; + } + + public boolean isPermanentAvailability() { + return this.availability == AVAILABILITY_PERMANENT || this.availability == AVAILABILITY_SHARED; + } + + public boolean isSharedAvailability() { + return this.availability == AVAILABILITY_SHARED; + } + + private static int normalizeAvailability(int value) { + if (value == AVAILABILITY_PERMANENT || value == AVAILABILITY_SHARED) { + return value; + } + + return AVAILABILITY_ROOM; + } + + static class JsonData { + String variableName; + boolean hasValue; + int availability; + + JsonData(String variableName, boolean hasValue, int availability) { + this.variableName = variableName; + this.hasValue = hasValue; + this.availability = availability; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java new file mode 100644 index 00000000..e063ec88 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java @@ -0,0 +1,800 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.HabboGender; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.Locale; + +public class WiredExtraVariableEcho extends InteractionWiredExtra { + public static final int CODE = 83; + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_ROOM = 3; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final int DEFAULT_USER_AVAILABILITY = WiredExtraUserVariable.AVAILABILITY_ROOM; + private static final int DEFAULT_FURNI_AVAILABILITY = WiredExtraFurniVariable.AVAILABILITY_ROOM_ACTIVE; + private static final int DEFAULT_ROOM_AVAILABILITY = WiredExtraRoomVariable.AVAILABILITY_ROOM_ACTIVE; + + private String variableName = ""; + private int sourceTargetType = TARGET_USER; + private String sourceVariableToken = ""; + private int sourceVariableItemId = 0; + private String sourceVariableName = ""; + + public WiredExtraVariableEcho(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableEcho(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + ConfigData config = parseConfigData(settings.getStringParam()); + int normalizedTargetType = normalizeTargetType(config.sourceTargetType); + String normalizedToken = normalizeVariableToken(config.sourceVariableToken, config.sourceVariableItemId); + int normalizedItemId = getCustomVariableItemId(normalizedToken); + SourceState sourceState = this.resolveSourceState(room, normalizedTargetType, normalizedToken, normalizedItemId); + + if (normalizedToken.isEmpty()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + if (sourceState == null || !sourceState.hasValue()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (!isAllowedEchoSource(sourceState, normalizedToken)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (!WiredVariableTextConnectorSupport.isTextConnected(room, this)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (createsCycle(room, this.getId(), normalizedTargetType, normalizedToken, normalizedItemId)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + String normalizedName = deriveVariableName(config.variableName, sourceState.getName()); + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.sourceTargetType = normalizedTargetType; + this.sourceVariableToken = normalizedToken; + this.sourceVariableItemId = normalizedItemId; + this.sourceVariableName = sourceState.getName(); + + room.getUserVariableManager().broadcastSnapshot(); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId, this.sourceVariableName)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(WiredManager.getGson().toJson(new EditorPayload(this.variableName, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId, this.getResolvedSourceName(room)))); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.sourceTargetType = normalizeTargetType(data.sourceTargetType); + this.sourceVariableToken = normalizeVariableToken(data.sourceVariableToken, data.sourceVariableItemId); + this.sourceVariableItemId = getCustomVariableItemId(this.sourceVariableToken); + this.sourceVariableName = normalizeSourceName(data.sourceVariableName); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.sourceTargetType = TARGET_USER; + this.sourceVariableToken = ""; + this.sourceVariableItemId = 0; + this.sourceVariableName = ""; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public int getSourceTargetType() { + return this.sourceTargetType; + } + + public String getSourceVariableToken() { + return this.sourceVariableToken; + } + + public int getSourceVariableItemId() { + return this.sourceVariableItemId; + } + + public String getSourceVariableName() { + return this.sourceVariableName; + } + + public boolean isUserEcho() { + return this.sourceTargetType == TARGET_USER; + } + + public boolean isFurniEcho() { + return this.sourceTargetType == TARGET_FURNI; + } + + public boolean isRoomEcho() { + return this.sourceTargetType == TARGET_ROOM; + } + + public WiredVariableDefinitionInfo createDefinitionInfo(Room room) { + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + int availability = (sourceState != null) ? sourceState.getAvailability() : defaultAvailability(this.sourceTargetType); + boolean hasValue = (sourceState == null) || sourceState.hasValue(); + boolean readOnly = sourceState == null || sourceState.isReadOnly(); + + return new WiredVariableDefinitionInfo( + this.getId(), + this.variableName, + hasValue, + availability, + WiredVariableTextConnectorSupport.isTextConnected(room, this), + readOnly + ); + } + + public boolean hasVariable(Room room, int entityId) { + if (room == null) { + return false; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().hasVariable(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().hasVariable(this.sourceVariableItemId); + default -> room.getUserVariableManager().hasVariable(entityId, this.sourceVariableItemId); + }; + } + + return this.readCurrentValue(room, entityId) != null; + } + + public int getCurrentValue(Room room, int entityId) { + Integer value = this.readCurrentValue(room, entityId); + return (value != null) ? value : 0; + } + + public int getCreatedAt(Room room, int entityId) { + if (room == null || !isCustomVariableToken(this.sourceVariableToken)) { + return 0; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getCreatedAt(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getCreatedAt(this.sourceVariableItemId); + default -> room.getUserVariableManager().getCreatedAt(entityId, this.sourceVariableItemId); + }; + } + + public int getUpdatedAt(Room room, int entityId) { + if (room == null || !isCustomVariableToken(this.sourceVariableToken)) { + return 0; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getUpdatedAt(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getUpdatedAt(this.sourceVariableItemId); + default -> room.getUserVariableManager().getUpdatedAt(entityId, this.sourceVariableItemId); + }; + } + + public boolean assignValue(Room room, int entityId, Integer value, boolean overrideExisting) { + if (room == null) { + return false; + } + + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + if (sourceState == null || sourceState.isReadOnly()) { + return false; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().assignVariable(room.getHabboItem(entityId), this.sourceVariableItemId, value, overrideExisting); + case TARGET_ROOM -> room.getRoomVariableManager().updateVariableValue(this.sourceVariableItemId, (value != null) ? value : 0); + default -> room.getUserVariableManager().assignVariable(room.getHabbo(entityId), this.sourceVariableItemId, value, overrideExisting); + }; + } + + return value != null && this.writeCurrentValue(room, entityId, value); + } + + public boolean updateValue(Room room, int entityId, Integer value) { + if (room == null) { + return false; + } + + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + if (sourceState == null || sourceState.isReadOnly() || !sourceState.hasValue()) { + return false; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().updateVariableValue(entityId, this.sourceVariableItemId, value); + case TARGET_ROOM -> room.getRoomVariableManager().updateVariableValue(this.sourceVariableItemId, (value != null) ? value : 0); + default -> room.getUserVariableManager().updateVariableValue(entityId, this.sourceVariableItemId, value); + }; + } + + return value != null && this.writeCurrentValue(room, entityId, value); + } + + public boolean removeValue(Room room, int entityId) { + if (room == null || !isCustomVariableToken(this.sourceVariableToken)) { + return false; + } + + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + if (sourceState == null || sourceState.isReadOnly()) { + return false; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().removeVariable(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().removeVariable(this.sourceVariableItemId); + default -> room.getUserVariableManager().removeVariable(entityId, this.sourceVariableItemId); + }; + } + + private Integer readCurrentValue(Room room, int entityId) { + if (room == null || this.sourceVariableToken == null || this.sourceVariableToken.isEmpty()) { + return null; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().hasVariable(entityId, this.sourceVariableItemId) + ? room.getFurniVariableManager().getCurrentValue(entityId, this.sourceVariableItemId) + : null; + case TARGET_ROOM -> room.getRoomVariableManager().getCurrentValue(this.sourceVariableItemId); + default -> room.getUserVariableManager().hasVariable(entityId, this.sourceVariableItemId) + ? room.getUserVariableManager().getCurrentValue(entityId, this.sourceVariableItemId) + : null; + }; + } + + String key = getInternalVariableKey(this.sourceVariableToken); + if (key.isEmpty()) { + return null; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> this.readFurniInternalValue(room, room.getHabboItem(entityId), key); + case TARGET_ROOM -> this.readRoomInternalValue(room, key); + default -> { + Habbo habbo = room.getHabbo(entityId); + yield this.readUserInternalValue(room, (habbo != null) ? habbo.getRoomUnit() : null, key); + } + }; + } + + private boolean writeCurrentValue(Room room, int entityId, int value) { + if (room == null || !isInternalVariableToken(this.sourceVariableToken)) { + return false; + } + + String key = getInternalVariableKey(this.sourceVariableToken); + if (key.isEmpty()) { + return false; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> this.writeFurniInternalValue(room, room.getHabboItem(entityId), key, value); + case TARGET_ROOM -> false; + default -> { + Habbo habbo = room.getHabbo(entityId); + yield this.writeUserInternalValue(room, (habbo != null) ? habbo.getRoomUnit() : null, key, value); + } + }; + } + + private SourceState resolveSourceState(Room room, int targetType, String token, int variableItemId) { + if (room == null || token == null || token.isEmpty()) { + return null; + } + + if (isCustomVariableToken(token)) { + WiredVariableDefinitionInfo definitionInfo = switch (targetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getDefinitionInfo(variableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getDefinitionInfo(variableItemId); + default -> room.getUserVariableManager().getDefinitionInfo(variableItemId); + }; + + if (definitionInfo == null) { + return null; + } + + return new SourceState(definitionInfo.getName(), definitionInfo.hasValue(), definitionInfo.getAvailability(), definitionInfo.isReadOnly()); + } + + String key = getInternalVariableKey(token); + if (key.isEmpty()) { + return null; + } + + return switch (targetType) { + case TARGET_FURNI -> canUseFurniInternalReference(key) + ? new SourceState(key, true, DEFAULT_FURNI_AVAILABILITY, !canUseFurniInternalDestination(key)) + : null; + case TARGET_ROOM -> canUseRoomInternalReference(key) + ? new SourceState(key, true, DEFAULT_ROOM_AVAILABILITY, true) + : null; + default -> canUseUserInternalReference(key) + ? new SourceState(key, true, DEFAULT_USER_AVAILABILITY, !canUseUserInternalDestination(key)) + : null; + }; + } + + private String getResolvedSourceName(Room room) { + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + return (sourceState != null) ? sourceState.getName() : this.sourceVariableName; + } + + private static boolean createsCycle(Room room, int currentItemId, int targetType, String token, int variableItemId) { + if (room == null || currentItemId <= 0 || !isCustomVariableToken(token) || variableItemId <= 0) { + return false; + } + + if (variableItemId == currentItemId) { + return true; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(room, targetType, variableItemId); + if (derivedDefinition != null) { + return createsCycle(room, currentItemId, targetType, createCustomVariableToken(derivedDefinition.getBaseDefinitionItemId()), derivedDefinition.getBaseDefinitionItemId()); + } + + if (room.getRoomSpecialTypes() == null) { + return false; + } + + InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(variableItemId); + if (!(extra instanceof WiredExtraVariableEcho)) { + return false; + } + + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + return createsCycle(room, currentItemId, echo.getSourceTargetType(), echo.getSourceVariableToken(), echo.getSourceVariableItemId()); + } + + private static String deriveVariableName(String requestedName, String sourceName) { + String normalizedRequestedName = WiredVariableNameValidator.normalizeForSave(requestedName); + if (!normalizedRequestedName.isEmpty()) { + return normalizedRequestedName; + } + + String fallbackValue = normalizeSourceName(sourceName) + .replaceAll("^[~@]+", "") + .replaceAll("[^A-Za-z0-9_]+", "_") + .replaceAll("_+", "_") + .replaceAll("^_+", "") + .replaceAll("_+$", ""); + + if (fallbackValue.length() > WiredVariableNameValidator.MAX_NAME_LENGTH) { + fallbackValue = fallbackValue.substring(0, WiredVariableNameValidator.MAX_NAME_LENGTH); + } + + return fallbackValue; + } + + private static boolean isAllowedEchoSource(SourceState sourceState, String token) { + if (sourceState == null || token == null || token.isEmpty()) { + return false; + } + + if (isInternalVariableToken(token)) { + return true; + } + + return isCustomVariableToken(token) && sourceState.getName() != null && sourceState.getName().contains("."); + } + + private static ConfigData parseConfigData(String value) { + if (value == null || value.isEmpty() || !value.startsWith("{")) { + return new ConfigData(); + } + + ConfigData config = WiredManager.getGson().fromJson(value, ConfigData.class); + return (config != null) ? config : new ConfigData(); + } + + private static String normalizeSourceName(String value) { + if (value == null) { + return ""; + } + + return value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + } + + private static int normalizeTargetType(int value) { + if (value == TARGET_FURNI || value == TARGET_ROOM) { + return value; + } + + return TARGET_USER; + } + + private static int defaultAvailability(int targetType) { + return switch (targetType) { + case TARGET_FURNI -> DEFAULT_FURNI_AVAILABILITY; + case TARGET_ROOM -> DEFAULT_ROOM_AVAILABILITY; + default -> DEFAULT_USER_AVAILABILITY; + }; + } + + private static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + private static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + private static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + private static int getCustomVariableItemId(String token) { + if (!isCustomVariableToken(token)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + private static String createCustomVariableToken(int itemId) { + return itemId > 0 ? CUSTOM_TOKEN_PREFIX + itemId : ""; + } + + private static String normalizeVariableToken(String token, int fallbackItemId) { + String normalizedToken = (token != null) ? token.trim() : ""; + + if (isCustomVariableToken(normalizedToken)) { + return normalizedToken; + } + + if (isInternalVariableToken(normalizedToken)) { + return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalizedToken.substring(INTERNAL_TOKEN_PREFIX.length())); + } + + if (fallbackItemId > 0) { + return createCustomVariableToken(fallbackItemId); + } + + if (normalizedToken.isEmpty()) { + return ""; + } + + try { + return createCustomVariableToken(Integer.parseInt(normalizedToken)); + } catch (NumberFormatException ignored) { + return ""; + } + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer getUserTypeValue(Habbo habbo, Bot bot, Pet pet) { + if (habbo != null) return 1; + if (bot != null) return 2; + if (pet != null) return 3; + + return null; + } + + private Integer getGenderValue(Habbo habbo, Bot bot) { + HabboGender gender = null; + + if (habbo != null && habbo.getHabboInfo() != null) { + gender = habbo.getHabboInfo().getGender(); + } else if (bot != null) { + gender = bot.getGender(); + } + + if (gender == null) { + return null; + } + + return gender == HabboGender.F ? 1 : 2; + } + + private boolean writeUserInternalValue(Room room, RoomUnit roomUnit, String key, int value) { + return WiredInternalVariableSupport.writeUserValue(room, roomUnit, key, value); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private boolean writeFurniInternalValue(Room room, HabboItem item, String key, int value) { + return WiredInternalVariableSupport.writeFurniValue(room, item, key, value); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private int getTeamColorId(int effectId) { + if (effectId >= 33 && effectId <= 36) return effectId - 32; + if (effectId >= 40 && effectId <= 43) return effectId - 39; + return 0; + } + + private int getTeamTypeId(int effectId) { + if (effectId >= 33 && effectId <= 36) return 1; + if (effectId >= 40 && effectId <= 43) return 2; + return 0; + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game game = room.getGame(com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame.class); + if (game != null) return game; + + game = room.getGame(com.eu.habbo.habbohotel.games.freeze.FreezeGame.class); + if (game != null) return game; + + return room.getGame(com.eu.habbo.habbohotel.games.wired.WiredGame.class); + } + + private boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y) { + if (room == null || roomUnit == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); + return WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), 0, true); + } + + private boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { + if (room == null || item == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + FurnitureMovementError error = room.moveFurniTo(item, targetTile, rotation, z, null, true, true); + return error == FurnitureMovementError.NONE; + } + + private static Integer parseInteger(String value) { + if (value == null || value.isEmpty()) return 0; + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return 0; + } + } + + private static boolean canUseUserInternalDestination(String key) { + return WiredInternalVariableSupport.canUseUserDestination(key); + } + + private static boolean canUseFurniInternalDestination(String key) { + return WiredInternalVariableSupport.canUseFurniDestination(key); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private Integer getRoomEntryMethodValue(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return null; + } + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + + if (roomEntryMethod == null || roomEntryMethod.trim().isEmpty()) { + return 0; + } + + return switch (roomEntryMethod.trim().toLowerCase(Locale.ROOT)) { + case "door" -> 1; + case "teleport" -> 2; + default -> 3; + }; + } + + static class JsonData { + String variableName; + int sourceTargetType; + String sourceVariableToken; + int sourceVariableItemId; + String sourceVariableName; + + JsonData(String variableName, int sourceTargetType, String sourceVariableToken, int sourceVariableItemId, String sourceVariableName) { + this.variableName = variableName; + this.sourceTargetType = sourceTargetType; + this.sourceVariableToken = sourceVariableToken; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + } + } + + static class ConfigData { + String variableName = ""; + int sourceTargetType = TARGET_USER; + String sourceVariableToken = ""; + int sourceVariableItemId = 0; + } + + static class EditorPayload extends ConfigData { + String sourceVariableName; + + EditorPayload(String variableName, int sourceTargetType, String sourceVariableToken, int sourceVariableItemId, String sourceVariableName) { + this.variableName = variableName; + this.sourceTargetType = sourceTargetType; + this.sourceVariableToken = sourceVariableToken; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + } + } + + private static class SourceState { + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean readOnly; + + private SourceState(String name, boolean hasValue, int availability, boolean readOnly) { + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.readOnly = readOnly; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableFilterBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableFilterBase.java new file mode 100644 index 00000000..a11da8db --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableFilterBase.java @@ -0,0 +1,757 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +public abstract class WiredExtraVariableFilterBase extends InteractionWiredExtra { + protected static final int TARGET_USER = 0; + protected static final int TARGET_FURNI = 1; + protected static final int TARGET_CONTEXT = 2; + protected static final int TARGET_ROOM = 3; + + protected static final int AMOUNT_CONSTANT = 0; + protected static final int AMOUNT_VARIABLE = 1; + protected static final int SOURCE_SECONDARY_SELECTED = 101; + + protected static final int SORT_VALUE_HIGHEST = 0; + protected static final int SORT_VALUE_LOWEST = 1; + protected static final int SORT_CREATION_OLDEST = 2; + protected static final int SORT_CREATION_LATEST = 3; + protected static final int SORT_UPDATE_OLDEST = 4; + protected static final int SORT_UPDATE_LATEST = 5; + + private static final int MAX_FILTER_AMOUNT = 10000; + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final String DELIM = "\t"; + + protected int sortBy = SORT_VALUE_HIGHEST; + protected int amountMode = AMOUNT_CONSTANT; + protected int amountConstantValue = 1; + protected int referenceTargetType = TARGET_USER; + protected int referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected String variableToken = ""; + protected int variableItemId = 0; + protected String referenceVariableToken = ""; + protected int referenceVariableItemId = 0; + protected final List referenceSelectedItems = new ArrayList<>(); + + protected WiredExtraVariableFilterBase(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + protected WiredExtraVariableFilterBase(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + protected abstract int getVariableTargetType(); + + protected abstract int getCode(); + + public List filterUsers(Room room, WiredContext ctx, Iterable values) { + if (room == null || ctx == null || this.getVariableTargetType() != TARGET_USER || this.variableToken.isEmpty()) { + return toUserList(values); + } + + int amount = this.resolveAmount(ctx, room); + if (amount <= 0) return new ArrayList<>(); + + List> matches = new ArrayList<>(); + + for (RoomUnit roomUnit : values) { + if (roomUnit == null) continue; + + MetricSnapshot metric = this.resolveUserMetric(room, roomUnit); + if (metric == null) continue; + + matches.add(new SortableEntry<>(roomUnit, metric)); + } + + matches.sort(this.metricComparator()); + return trimUsers(matches, amount); + } + + public List filterItems(Room room, WiredContext ctx, Iterable values) { + if (room == null || ctx == null || this.getVariableTargetType() != TARGET_FURNI || this.variableToken.isEmpty()) { + return toItemList(values); + } + + int amount = this.resolveAmount(ctx, room); + if (amount <= 0) return new ArrayList<>(); + + List> matches = new ArrayList<>(); + + for (HabboItem item : values) { + if (item == null) continue; + + MetricSnapshot metric = this.resolveFurniMetric(room, item); + if (metric == null) continue; + + matches.add(new SortableEntry<>(item, metric)); + } + + matches.sort(this.metricComparator()); + return trimItems(matches, amount); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) throw new WiredSaveException("Room not found"); + + int[] params = settings.getIntParams(); + String[] stringParts = parseStringData(settings.getStringParam()); + String nextVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : ""); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + int nextSortBy = normalizeSortBy(param(params, 0, SORT_VALUE_HIGHEST)); + int nextAmountMode = normalizeAmountMode(param(params, 1, AMOUNT_CONSTANT)); + int nextAmountConstantValue = normalizeAmount(param(params, 2, 1)); + int nextReferenceTargetType = normalizeReferenceTargetType(param(params, 3, TARGET_USER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 4, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + + if (!this.isValidMainVariable(room, nextVariableToken)) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + if (nextAmountMode == AMOUNT_VARIABLE && !this.isValidReference(room, nextReferenceTargetType, nextReferenceVariableToken)) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + + List nextReferenceItems = new ArrayList<>(); + if (nextAmountMode == AMOUNT_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) { + int selectionLimit = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); + if (settings.getFurniIds().length > selectionLimit) throw new WiredSaveException("Too many furni selected"); + + for (int furniId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(furniId); + if (item != null) nextReferenceItems.add(item); + } + } + + this.sortBy = nextSortBy; + this.amountMode = nextAmountMode; + this.amountConstantValue = nextAmountConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.setVariableToken(nextVariableToken); + this.setReferenceVariableToken(nextReferenceVariableToken); + this.referenceSelectedItems.clear(); + this.referenceSelectedItems.addAll(nextReferenceItems); + return true; + } + + @Override + public String getWiredData() { + this.refreshReferenceItems(); + return WiredManager.getGson().toJson(new JsonData(this.sortBy, this.amountMode, this.amountConstantValue, this.referenceTargetType, this.referenceUserSource, this.referenceFurniSource, this.variableToken, this.variableItemId, this.referenceVariableToken, this.referenceVariableItemId, this.toIds(this.referenceSelectedItems))); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refreshReferenceItems(); + List selectedItems = new ArrayList<>(); + if (this.amountMode == AMOUNT_VARIABLE && this.referenceTargetType == TARGET_FURNI && this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) { + selectedItems.addAll(this.referenceSelectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + for (HabboItem item : selectedItems) message.appendInt(item.getId()); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(6); + message.appendInt(this.sortBy); + message.appendInt(this.amountMode); + message.appendInt(this.amountConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(0); + message.appendInt(this.getCode()); + message.appendInt(0); + message.appendInt(0); + } + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.sortBy = normalizeSortBy(data.sortBy); + this.amountMode = normalizeAmountMode(data.amountMode); + this.amountConstantValue = normalizeAmount(data.amountConstantValue); + this.referenceTargetType = normalizeReferenceTargetType(data.referenceTargetType); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + + if (room == null || data.selectedItemIds == null) return; + + for (Integer itemId : data.selectedItemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.referenceSelectedItems.add(item); + } + } + + @Override + public void onPickUp() { + this.sortBy = SORT_VALUE_HIGHEST; + this.amountMode = AMOUNT_CONSTANT; + this.amountConstantValue = 1; + this.referenceTargetType = TARGET_USER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceSelectedItems.clear(); + this.setVariableToken(""); + this.setReferenceVariableToken(""); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + private int resolveAmount(WiredContext ctx, Room room) { + if (this.amountMode != AMOUNT_VARIABLE) return normalizeAmount(this.amountConstantValue); + + Integer value = this.resolveReferenceValue(ctx, room); + return value == null ? 0 : normalizeAmount(value); + } + + private Integer resolveReferenceValue(WiredContext ctx, Room room) { + if (room == null) return null; + + if (this.referenceTargetType == TARGET_FURNI) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.refreshReferenceItems(); + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + Integer value = this.readFurniReferenceValue(room, item); + if (value != null) return value; + } + + return null; + } + + if (this.referenceTargetType == TARGET_CONTEXT) { + return this.readContextReferenceValue(ctx, room); + } + + if (this.referenceTargetType == TARGET_ROOM) { + return this.readRoomReferenceValue(room); + } + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + Integer value = this.readUserReferenceValue(room, roomUnit); + if (value != null) return value; + } + + return null; + } + + private Integer readUserReferenceValue(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null) ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId) : null; + } + + private Integer readFurniReferenceValue(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + return (definition != null && definition.hasValue()) ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId) : null; + } + + private Integer readRoomReferenceValue(Room room) { + if (room == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseRoomInternalReference(key) ? this.readRoomInternalValue(room, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + return (definition != null && definition.hasValue()) ? room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId) : null; + } + + private Integer readContextReferenceValue(WiredContext ctx, Room room) { + if (ctx == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseContextInternalReference(key) ? WiredInternalVariableSupport.readContextValue(ctx, key) : null; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + return WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + } + + private MetricSnapshot resolveUserMetric(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + Integer value = canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + return (value != null) ? new MetricSnapshot(roomUnit.getId(), value, 0, 0) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + Habbo habbo = room.getHabbo(roomUnit); + if (definition == null || habbo == null) return null; + if (!room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId)) return null; + + return new MetricSnapshot( + roomUnit.getId(), + definition.hasValue() ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.variableItemId) : 0, + room.getUserVariableManager().getCreatedAt(habbo.getHabboInfo().getId(), this.variableItemId), + room.getUserVariableManager().getUpdatedAt(habbo.getHabboInfo().getId(), this.variableItemId)); + } + + private MetricSnapshot resolveFurniMetric(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + Integer value = canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + return (value != null) ? new MetricSnapshot(item.getId(), value, 0, 0) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + if (definition == null) return null; + if (!room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId)) return null; + + return new MetricSnapshot( + item.getId(), + definition.hasValue() ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.variableItemId) : 0, + room.getFurniVariableManager().getCreatedAt(item.getId(), this.variableItemId), + room.getFurniVariableManager().getUpdatedAt(item.getId(), this.variableItemId)); + } + + private Comparator> metricComparator() { + return switch (this.sortBy) { + case SORT_VALUE_LOWEST -> Comparator.comparingInt((SortableEntry entry) -> entry.metric.value).thenComparingInt(entry -> entry.metric.entityId); + case SORT_CREATION_OLDEST -> Comparator.comparingInt((SortableEntry entry) -> entry.metric.createdAt).thenComparingInt(entry -> entry.metric.entityId); + case SORT_CREATION_LATEST -> Comparator., Integer>comparing(entry -> entry.metric.createdAt).reversed().thenComparingInt(entry -> entry.metric.entityId); + case SORT_UPDATE_OLDEST -> Comparator.comparingInt((SortableEntry entry) -> entry.metric.updatedAt).thenComparingInt(entry -> entry.metric.entityId); + case SORT_UPDATE_LATEST -> Comparator., Integer>comparing(entry -> entry.metric.updatedAt).reversed().thenComparingInt(entry -> entry.metric.entityId); + default -> Comparator., Integer>comparing(entry -> entry.metric.value).reversed().thenComparingInt(entry -> entry.metric.entityId); + }; + } + + private boolean isValidMainVariable(Room room, String token) { + if (token == null || token.isEmpty()) return false; + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return this.getVariableTargetType() == TARGET_FURNI ? canUseFurniInternalReference(key) : canUseUserInternalReference(key); + } + + if (this.getVariableTargetType() == TARGET_FURNI) { + return room != null && room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) != null; + } + + return room != null && room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) != null; + } + + private boolean isValidReference(Room room, int targetType, String token) { + if (token == null || token.isEmpty()) return false; + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return switch (targetType) { + case TARGET_FURNI -> canUseFurniInternalReference(key); + case TARGET_CONTEXT -> canUseContextInternalReference(key); + case TARGET_ROOM -> canUseRoomInternalReference(key); + default -> canUseUserInternalReference(key); + }; + } + + return switch (targetType) { + case TARGET_FURNI -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + case TARGET_CONTEXT -> this.isValidContextCustomReference(room, getCustomItemId(token)); + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + default -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + }; + } + + private boolean isValidContextCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + private static List trimUsers(List> matches, int amount) { + List result = new ArrayList<>(); + for (SortableEntry match : matches) { + if (result.size() >= amount) break; + result.add(match.entity); + } + return result; + } + + private static List trimItems(List> matches, int amount) { + List result = new ArrayList<>(); + for (SortableEntry match : matches) { + if (result.size() >= amount) break; + result.add(match.entity); + } + return result; + } + + private static List toUserList(Iterable values) { + List result = new ArrayList<>(); + if (values == null) return result; + for (RoomUnit value : values) if (value != null) result.add(value); + return result; + } + + private static List toItemList(Iterable values) { + List result = new ArrayList<>(); + if (values == null) return result; + for (HabboItem value : values) if (value != null) result.add(value); + return result; + } + + private String serializeStringData() { + return (this.variableToken == null ? "" : this.variableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken); + } + + private void refreshReferenceItems() { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + this.referenceSelectedItems.clear(); + return; + } + + this.referenceSelectedItems.removeIf(item -> item == null || item.getRoomId() != room.getId() || room.getHabboItem(item.getId()) == null); + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private List toIds(List items) { + List ids = new ArrayList<>(); + for (HabboItem item : items) if (item != null) ids.add(item.getId()); + return ids; + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + private int getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getGamePlayer() == null) return 0; + + Game game = this.resolveTeamGame(room, habbo); + if (game == null) return 0; + + GamePlayer player = habbo.getHabboInfo().getGamePlayer(); + return player.getScore(); + } + + private int getTeamColorId(int effectValue) { + TeamEffectData effectData = this.getTeamEffectData(effectValue); + return (effectData != null) ? effectData.colorId : 0; + } + + private int getTeamTypeId(int effectValue) { + TeamEffectData effectData = this.getTeamEffectData(effectValue); + return (effectData != null) ? effectData.typeId : 0; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = room.getGame(WiredGame.class); + if (game == null) game = room.getGame(FreezeGame.class); + if (game == null) game = room.getGame(BattleBanzaiGame.class); + if (game == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + private static int normalizeSortBy(int value) { + return switch (value) { + case SORT_VALUE_LOWEST, SORT_CREATION_OLDEST, SORT_CREATION_LATEST, SORT_UPDATE_OLDEST, SORT_UPDATE_LATEST -> value; + default -> SORT_VALUE_HIGHEST; + }; + } + + private static int normalizeAmountMode(int value) { + return (value == AMOUNT_VARIABLE) ? AMOUNT_VARIABLE : AMOUNT_CONSTANT; + } + + private static int normalizeReferenceTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + protected static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (normalized.startsWith(INTERNAL_TOKEN_PREFIX)) { + return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + } + if (isCustomVariableToken(normalized) || isInternalVariableToken(normalized)) return normalized; + + try { + int parsed = Integer.parseInt(normalized); + return (parsed > 0) ? (CUSTOM_TOKEN_PREFIX + parsed) : ""; + } catch (NumberFormatException e) { + return ""; + } + } + + protected static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + protected static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + protected static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException e) { + return 0; + } + } + + protected static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params != null && params.length > index) ? params[index] : fallback; + } + + private static String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private static int normalizeAmount(int value) { + return Math.max(0, Math.min(MAX_FILTER_AMOUNT, value)); + } + + protected static class JsonData { + int sortBy; + int amountMode; + int amountConstantValue; + int referenceTargetType; + int referenceUserSource; + int referenceFurniSource; + String variableToken; + int variableItemId; + String referenceVariableToken; + int referenceVariableItemId; + List selectedItemIds; + + JsonData(int sortBy, int amountMode, int amountConstantValue, int referenceTargetType, int referenceUserSource, int referenceFurniSource, String variableToken, int variableItemId, String referenceVariableToken, int referenceVariableItemId, List selectedItemIds) { + this.sortBy = sortBy; + this.amountMode = amountMode; + this.amountConstantValue = amountConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.selectedItemIds = selectedItemIds; + } + } + + private static class SortableEntry { + final T entity; + final MetricSnapshot metric; + + SortableEntry(T entity, MetricSnapshot metric) { + this.entity = entity; + this.metric = metric; + } + } + + private static class MetricSnapshot { + final int entityId; + final int value; + final int createdAt; + final int updatedAt; + + MetricSnapshot(int entityId, int value, int createdAt, int updatedAt) { + this.entityId = entityId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableLevelUpSystem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableLevelUpSystem.java new file mode 100644 index 00000000..12afd167 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableLevelUpSystem.java @@ -0,0 +1,270 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredExtraVariableLevelUpSystem extends InteractionWiredExtra { + public static final int CODE = 82; + + public static final int MODE_LINEAR = 1; + public static final int MODE_EXPONENTIAL = 2; + public static final int MODE_MANUAL = 3; + + public static final int SUB_CURRENT_LEVEL = 0; + public static final int SUB_CURRENT_XP = 1; + public static final int SUB_LEVEL_PROGRESS = 2; + public static final int SUB_LEVEL_PROGRESS_PERCENT = 3; + public static final int SUB_TOTAL_XP_REQUIRED = 4; + public static final int SUB_XP_REMAINING = 5; + public static final int SUB_IS_AT_MAX = 6; + public static final int SUB_MAX_LEVEL = 7; + public static final int SUBVARIABLE_COUNT = 8; + + private static final int DEFAULT_STEP_SIZE = 100; + private static final int DEFAULT_MAX_LEVEL = 10; + private static final int DEFAULT_FIRST_LEVEL_XP = 100; + private static final int DEFAULT_INCREASE_FACTOR = 100; + private static final int MAX_MANUAL_TEXT_LENGTH = 4096; + + private int mode = MODE_LINEAR; + private int stepSize = DEFAULT_STEP_SIZE; + private int maxLevel = DEFAULT_MAX_LEVEL; + private int firstLevelXp = DEFAULT_FIRST_LEVEL_XP; + private int increaseFactor = DEFAULT_INCREASE_FACTOR; + private String interpolationText = ""; + private int subvariableMask = (1 << SUB_CURRENT_LEVEL) | (1 << SUB_CURRENT_XP); + + public WiredExtraVariableLevelUpSystem(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableLevelUpSystem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + this.applyConfig(parseJsonData(settings.getStringParam())); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.mode, + this.stepSize, + this.maxLevel, + this.firstLevelXp, + this.increaseFactor, + this.interpolationText, + this.getSelectedSubvariables() + )); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.getWiredData()); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + this.applyConfig(parseJsonData(wiredData)); + } + + @Override + public void onPickUp() { + this.mode = MODE_LINEAR; + this.stepSize = DEFAULT_STEP_SIZE; + this.maxLevel = DEFAULT_MAX_LEVEL; + this.firstLevelXp = DEFAULT_FIRST_LEVEL_XP; + this.increaseFactor = DEFAULT_INCREASE_FACTOR; + this.interpolationText = ""; + this.subvariableMask = (1 << SUB_CURRENT_LEVEL) | (1 << SUB_CURRENT_XP); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getMode() { + return this.mode; + } + + public int getStepSize() { + return this.stepSize; + } + + public int getMaxLevel() { + return this.maxLevel; + } + + public int getFirstLevelXp() { + return this.firstLevelXp; + } + + public int getIncreaseFactor() { + return this.increaseFactor; + } + + public String getInterpolationText() { + return this.interpolationText; + } + + public boolean hasSubvariable(int subvariableType) { + return subvariableType >= 0 + && subvariableType < SUBVARIABLE_COUNT + && ((this.subvariableMask & (1 << subvariableType)) != 0); + } + + public List getSelectedSubvariables() { + List result = new ArrayList<>(); + + for (int index = 0; index < SUBVARIABLE_COUNT; index++) { + if (this.hasSubvariable(index)) { + result.add(index); + } + } + + return result; + } + + private void applyConfig(JsonData data) { + if (data == null) { + this.onPickUp(); + return; + } + + this.mode = normalizeMode(data.mode); + this.stepSize = normalizeNonNegative(data.stepSize, DEFAULT_STEP_SIZE); + this.maxLevel = normalizeMaxLevel(data.maxLevel); + this.firstLevelXp = normalizeNonNegative(data.firstLevelXp, DEFAULT_FIRST_LEVEL_XP); + this.increaseFactor = normalizeNonNegative(data.increaseFactor, DEFAULT_INCREASE_FACTOR); + this.interpolationText = normalizeInterpolationText(data.interpolationText); + this.subvariableMask = normalizeSubvariableMask(data.subvariables); + } + + private static JsonData parseJsonData(String value) { + if (value == null || value.trim().isEmpty()) { + return new JsonData(); + } + + try { + if (value.trim().startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(value, JsonData.class); + return (data != null) ? data : new JsonData(); + } + } catch (Exception ignored) { + } + + JsonData fallback = new JsonData(); + fallback.interpolationText = normalizeInterpolationText(value); + fallback.mode = MODE_MANUAL; + return fallback; + } + + private static int normalizeMode(int value) { + return switch (value) { + case MODE_EXPONENTIAL, MODE_MANUAL -> value; + default -> MODE_LINEAR; + }; + } + + private static int normalizeNonNegative(int value, int fallback) { + return Math.max(0, (value > 0) ? value : fallback); + } + + private static int normalizeMaxLevel(int value) { + return Math.max(1, (value > 0) ? value : DEFAULT_MAX_LEVEL); + } + + private static String normalizeInterpolationText(String value) { + if (value == null) { + return ""; + } + + String normalized = value.replace("\r", ""); + if (normalized.length() > MAX_MANUAL_TEXT_LENGTH) { + normalized = normalized.substring(0, MAX_MANUAL_TEXT_LENGTH); + } + + return normalized; + } + + private static int normalizeSubvariableMask(List subvariables) { + if (subvariables == null) { + return (1 << SUB_CURRENT_LEVEL) | (1 << SUB_CURRENT_XP); + } + + int mask = 0; + for (Integer subvariable : subvariables) { + if (subvariable == null || subvariable < 0 || subvariable >= SUBVARIABLE_COUNT) { + continue; + } + + mask |= (1 << subvariable); + } + + return mask; + } + + static class JsonData { + int mode = MODE_LINEAR; + int stepSize = DEFAULT_STEP_SIZE; + int maxLevel = DEFAULT_MAX_LEVEL; + int firstLevelXp = DEFAULT_FIRST_LEVEL_XP; + int increaseFactor = DEFAULT_INCREASE_FACTOR; + String interpolationText = ""; + List subvariables = null; + + JsonData() { + } + + JsonData(int mode, int stepSize, int maxLevel, int firstLevelXp, int increaseFactor, String interpolationText, List subvariables) { + this.mode = mode; + this.stepSize = stepSize; + this.maxLevel = maxLevel; + this.firstLevelXp = firstLevelXp; + this.increaseFactor = increaseFactor; + this.interpolationText = interpolationText; + this.subvariables = subvariables; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableReference.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableReference.java new file mode 100644 index 00000000..96970464 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableReference.java @@ -0,0 +1,321 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredExtraVariableReference extends InteractionWiredExtra { + public static final int CODE = 81; + + private String variableName = ""; + private int sourceRoomId = 0; + private String sourceRoomName = ""; + private int sourceVariableItemId = 0; + private String sourceVariableName = ""; + private int sourceTargetType = WiredVariableReferenceSupport.TARGET_USER; + private boolean hasValue = false; + private boolean readOnly = true; + + public WiredExtraVariableReference(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableReference(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + ConfigData config = parseConfigData(settings.getStringParam()); + String normalizedName = WiredVariableNameValidator.normalizeForSave(config.variableName); + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + if (config.sourceRoomId <= 0 || config.sourceVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + WiredVariableReferenceSupport.SharedDefinitionOption definition = WiredVariableReferenceSupport.findSharedDefinition( + room, + config.sourceRoomId, + config.sourceVariableItemId, + config.sourceTargetType + ); + + if (definition == null) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + this.variableName = normalizedName; + this.sourceRoomId = definition.getRoomId(); + this.sourceRoomName = sanitizeLabel(definition.getRoomName()); + this.sourceVariableItemId = definition.getItemId(); + this.sourceVariableName = definition.getName(); + this.sourceTargetType = definition.getTargetType(); + this.hasValue = definition.hasValue(); + this.readOnly = config.readOnly; + + room.getUserVariableManager().broadcastSnapshot(); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.variableName, + this.sourceRoomId, + this.sourceRoomName, + this.sourceVariableItemId, + this.sourceVariableName, + this.sourceTargetType, + this.hasValue, + this.readOnly + )); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(buildEditorPayload(room)); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.sourceRoomId = Math.max(0, data.sourceRoomId); + this.sourceRoomName = sanitizeLabel(data.sourceRoomName); + this.sourceVariableItemId = Math.max(0, data.sourceVariableItemId); + this.sourceVariableName = WiredVariableNameValidator.normalizeLegacy(data.sourceVariableName); + this.sourceTargetType = normalizeTargetType(data.sourceTargetType); + this.hasValue = data.hasValue; + this.readOnly = data.readOnly; + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.sourceRoomId = 0; + this.sourceRoomName = ""; + this.sourceVariableItemId = 0; + this.sourceVariableName = ""; + this.sourceTargetType = WiredVariableReferenceSupport.TARGET_USER; + this.hasValue = false; + this.readOnly = true; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public int getSourceRoomId() { + return this.sourceRoomId; + } + + public String getSourceRoomName() { + return this.sourceRoomName; + } + + public int getSourceVariableItemId() { + return this.sourceVariableItemId; + } + + public String getSourceVariableName() { + return this.sourceVariableName; + } + + public int getSourceTargetType() { + return this.sourceTargetType; + } + + public boolean hasValue() { + return this.hasValue; + } + + public boolean isReadOnly() { + return this.readOnly; + } + + public int getAvailability() { + return WiredVariableReferenceSupport.SHARED_AVAILABILITY; + } + + public boolean isUserReference() { + return this.sourceTargetType == WiredVariableReferenceSupport.TARGET_USER; + } + + public boolean isRoomReference() { + return this.sourceTargetType == WiredVariableReferenceSupport.TARGET_ROOM; + } + + private String buildEditorPayload(Room room) { + List roomOptions = new ArrayList<>(); + + for (WiredVariableReferenceSupport.RoomOption option : WiredVariableReferenceSupport.loadRoomOptions(room)) { + List variables = new ArrayList<>(); + + for (WiredVariableReferenceSupport.SharedDefinitionOption definition : option.getVariables()) { + variables.add(new VariableEditorData(definition.getItemId(), definition.getName(), definition.getTargetType(), definition.hasValue())); + } + + roomOptions.add(new RoomEditorData(option.getRoomId(), option.getRoomName(), variables)); + } + + return WiredManager.getGson().toJson(new EditorPayload( + this.variableName, + this.sourceRoomId, + this.sourceRoomName, + this.sourceVariableItemId, + this.sourceVariableName, + this.sourceTargetType, + this.readOnly, + roomOptions + )); + } + + private static ConfigData parseConfigData(String value) { + if (value == null || value.isEmpty() || !value.startsWith("{")) { + return new ConfigData(); + } + + ConfigData config = WiredManager.getGson().fromJson(value, ConfigData.class); + return (config != null) ? config : new ConfigData(); + } + + private static int normalizeTargetType(int value) { + return (value == WiredVariableReferenceSupport.TARGET_ROOM) ? WiredVariableReferenceSupport.TARGET_ROOM : WiredVariableReferenceSupport.TARGET_USER; + } + + private static String sanitizeLabel(String value) { + if (value == null) { + return ""; + } + + return value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + } + + static class JsonData { + String variableName; + int sourceRoomId; + String sourceRoomName; + int sourceVariableItemId; + String sourceVariableName; + int sourceTargetType; + boolean hasValue; + boolean readOnly; + + JsonData(String variableName, int sourceRoomId, String sourceRoomName, int sourceVariableItemId, String sourceVariableName, int sourceTargetType, boolean hasValue, boolean readOnly) { + this.variableName = variableName; + this.sourceRoomId = sourceRoomId; + this.sourceRoomName = sourceRoomName; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + this.sourceTargetType = sourceTargetType; + this.hasValue = hasValue; + this.readOnly = readOnly; + } + } + + static class ConfigData { + String variableName = ""; + int sourceRoomId = 0; + int sourceVariableItemId = 0; + int sourceTargetType = WiredVariableReferenceSupport.TARGET_USER; + boolean readOnly = true; + } + + static class EditorPayload extends ConfigData { + String sourceRoomName; + String sourceVariableName; + List rooms; + + EditorPayload(String variableName, int sourceRoomId, String sourceRoomName, int sourceVariableItemId, String sourceVariableName, int sourceTargetType, boolean readOnly, List rooms) { + this.variableName = variableName; + this.sourceRoomId = sourceRoomId; + this.sourceRoomName = sourceRoomName; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + this.sourceTargetType = sourceTargetType; + this.readOnly = readOnly; + this.rooms = rooms; + } + } + + static class RoomEditorData { + int roomId; + String roomName; + List variables; + + RoomEditorData(int roomId, String roomName, List variables) { + this.roomId = roomId; + this.roomName = roomName; + this.variables = variables; + } + } + + static class VariableEditorData { + int itemId; + String name; + int targetType; + boolean hasValue; + + VariableEditorData(int itemId, String name, int targetType, boolean hasValue) { + this.itemId = itemId; + this.name = name; + this.targetType = targetType; + this.hasValue = hasValue; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java new file mode 100644 index 00000000..eb419c03 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java @@ -0,0 +1,204 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public class WiredExtraVariableTextConnector extends InteractionWiredExtra { + public static final int CODE = 79; + private static final int MAX_MAPPING_LENGTH = 4096; + + private String mappingsText = ""; + private LinkedHashMap mappings = new LinkedHashMap<>(); + + public WiredExtraVariableTextConnector(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableTextConnector(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + this.setMappingsText(settings.getStringParam()); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.mappingsText)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.mappingsText); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.setMappingsText(data.mappingsText); + } + + return; + } + + this.setMappingsText(wiredData); + } + + @Override + public void onPickUp() { + this.mappingsText = ""; + this.mappings = new LinkedHashMap<>(); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getMappingsText() { + return this.mappingsText; + } + + public Map getMappings() { + return Collections.unmodifiableMap(this.mappings); + } + + public String resolveText(Integer value) { + if (value == null) { + return ""; + } + + String mappedValue = this.mappings.get(value); + return mappedValue != null ? mappedValue : String.valueOf(value); + } + + public Integer resolveValue(String text) { + if (text == null) { + return null; + } + + String normalizedText = text.trim(); + if (normalizedText.isEmpty()) { + return null; + } + + for (Map.Entry entry : this.mappings.entrySet()) { + if (entry == null || entry.getKey() == null || entry.getValue() == null) { + continue; + } + + if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) { + return entry.getKey(); + } + } + + return null; + } + + private void setMappingsText(String value) { + this.mappingsText = normalizeMappingsText(value); + this.mappings = parseMappings(this.mappingsText); + } + + private static String normalizeMappingsText(String value) { + if (value == null) { + return ""; + } + + String normalized = value.replace("\r", ""); + + if (normalized.length() > MAX_MAPPING_LENGTH) { + normalized = normalized.substring(0, MAX_MAPPING_LENGTH); + } + + return normalized; + } + + private static LinkedHashMap parseMappings(String value) { + LinkedHashMap result = new LinkedHashMap<>(); + if (value == null || value.isEmpty()) { + return result; + } + + for (String rawLine : value.split("\n")) { + if (rawLine == null) { + continue; + } + + String line = rawLine.trim(); + if (line.isEmpty()) { + continue; + } + + int separatorIndex = line.indexOf('='); + if (separatorIndex < 0) { + separatorIndex = line.indexOf(','); + } + + if (separatorIndex <= 0) { + continue; + } + + String keyPart = line.substring(0, separatorIndex).trim(); + String valuePart = line.substring(separatorIndex + 1).trim(); + + try { + result.put(Integer.parseInt(keyPart), valuePart); + } catch (NumberFormatException ignored) { + } + } + + return result; + } + + static class JsonData { + String mappingsText; + + JsonData(String mappingsText) { + this.mappingsText = mappingsText; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java new file mode 100644 index 00000000..0efcea57 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java @@ -0,0 +1,110 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.util.regex.Pattern; + +final class WiredVariableNameValidator { + static final int MIN_NAME_LENGTH = 1; + static final int MAX_NAME_LENGTH = 40; + + private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + + private WiredVariableNameValidator() { + } + + static String normalizeForSave(String value) { + if (value == null) { + return ""; + } + + return value.trim() + .replace("\t", "") + .replace("\r", "") + .replace("\n", ""); + } + + static String normalizeLegacy(String value) { + String normalized = normalizeForSave(value); + + if (normalized.contains("=")) { + normalized = normalized.substring(0, normalized.indexOf('=')).trim(); + } + + while (normalized.startsWith("@") || normalized.startsWith("~")) { + normalized = normalized.substring(1).trim(); + } + + if (normalized.length() > MAX_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_NAME_LENGTH); + } + + return normalized; + } + + static void validateDefinitionName(Room room, int currentItemId, String variableName) throws WiredSaveException { + String normalized = normalizeForSave(variableName); + + if (normalized.length() < MIN_NAME_LENGTH || normalized.length() > MAX_NAME_LENGTH) { + throw new WiredSaveException("wiredfurni.error.variables.name_length"); + } + + if (!VALID_NAME_PATTERN.matcher(normalized).matches()) { + throw new WiredSaveException("wiredfurni.error.variables.name_syntax"); + } + + if (isNameInUse(room, currentItemId, normalized)) { + throw new WiredSaveException("wiredfurni.error.variables.name_uniq"); + } + } + + private static boolean isNameInUse(Room room, int currentItemId, String variableName) { + if (room == null || room.getRoomSpecialTypes() == null || variableName == null || variableName.isEmpty()) { + return false; + } + + for (InteractionWiredExtra extra : room.getRoomSpecialTypes().getExtras()) { + if (extra == null || extra.getId() == currentItemId) { + continue; + } + + String existingName = getDefinitionName(extra); + + if (existingName != null && existingName.equalsIgnoreCase(variableName)) { + return true; + } + } + + return false; + } + + private static String getDefinitionName(InteractionWiredExtra extra) { + if (extra instanceof WiredExtraUserVariable) { + return ((WiredExtraUserVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraFurniVariable) { + return ((WiredExtraFurniVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraRoomVariable) { + return ((WiredExtraRoomVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraContextVariable) { + return ((WiredExtraContextVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraVariableReference) { + return ((WiredExtraVariableReference) extra).getVariableName(); + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getVariableName(); + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java new file mode 100644 index 00000000..0d2641e7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java @@ -0,0 +1,629 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public final class WiredVariableReferenceSupport { + public static final int TARGET_USER = 0; + public static final int TARGET_ROOM = 3; + public static final int SHARED_AVAILABILITY = 11; + + private static final Logger LOGGER = LoggerFactory.getLogger(WiredVariableReferenceSupport.class); + + private static final ConcurrentHashMap USER_ASSIGNMENT_CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap ROOM_ASSIGNMENT_CACHE = new ConcurrentHashMap<>(); + + private WiredVariableReferenceSupport() { + } + + public static boolean isSharedAvailability(int availability) { + return availability == SHARED_AVAILABILITY; + } + + public static SharedDefinitionOption findSharedDefinition(Room room, int sourceRoomId, int sourceVariableItemId, int sourceTargetType) { + if (room == null || sourceRoomId <= 0 || sourceVariableItemId <= 0) { + return null; + } + + for (RoomOption roomOption : loadRoomOptions(room)) { + if (roomOption.getRoomId() != sourceRoomId) { + continue; + } + + for (SharedDefinitionOption definition : roomOption.getVariables()) { + if (definition.getItemId() == sourceVariableItemId && definition.getTargetType() == sourceTargetType) { + return definition; + } + } + } + + return null; + } + + public static List loadRoomOptions(Room room) { + if (room == null || room.getOwnerId() <= 0) { + return Collections.emptyList(); + } + + Map optionsByRoomId = new LinkedHashMap<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT rooms.id AS room_id, rooms.name AS room_name, items.id AS item_id, items.wired_data, items_base.interaction_type " + + "FROM rooms " + + "INNER JOIN items ON rooms.id = items.room_id " + + "INNER JOIN items_base ON items.item_id = items_base.id " + + "WHERE rooms.owner_id = ? AND rooms.id <> ? AND items_base.interaction_type IN ('wf_var_user', 'wf_var_room') " + + "ORDER BY rooms.name ASC, items.id ASC")) { + statement.setInt(1, room.getOwnerId()); + statement.setInt(2, room.getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + SharedDefinitionOption definition = parseSharedDefinition( + set.getString("interaction_type"), + set.getInt("item_id"), + set.getString("wired_data"), + set.getInt("room_id"), + set.getString("room_name") + ); + + if (definition == null) { + continue; + } + + RoomOption roomOption = optionsByRoomId.computeIfAbsent( + definition.getRoomId(), + key -> new RoomOption(definition.getRoomId(), definition.getRoomName(), new ArrayList<>()) + ); + + roomOption.getVariables().add(definition); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to load shared variable reference options for room {}", room.getId(), e); + } + + List result = new ArrayList<>(optionsByRoomId.values()); + + for (RoomOption option : result) { + option.getVariables().sort(Comparator.comparing(SharedDefinitionOption::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(SharedDefinitionOption::getItemId)); + } + + result.sort(Comparator.comparing(RoomOption::getRoomName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(RoomOption::getRoomId)); + return result; + } + + public static SharedUserAssignment getSharedUserAssignment(WiredExtraVariableReference reference, int userId) { + if (reference == null || !reference.isUserReference() || userId <= 0) { + return null; + } + + String cacheKey = createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId); + CachedUserAssignment cachedValue = USER_ASSIGNMENT_CACHE.get(cacheKey); + + if (cachedValue != null) { + return cachedValue.present ? cachedValue.toAssignment() : null; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT value, created_at, updated_at FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ? LIMIT 1")) { + statement.setInt(1, reference.getSourceRoomId()); + statement.setInt(2, userId); + statement.setInt(3, reference.getSourceVariableItemId()); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + USER_ASSIGNMENT_CACHE.put(cacheKey, CachedUserAssignment.missing()); + return null; + } + + Integer value = null; + int rawValue = set.getInt("value"); + if (!set.wasNull()) { + value = rawValue; + } + + int createdAt = normalizeTimestamp(set.getInt("created_at"), 0); + SharedUserAssignment assignment = new SharedUserAssignment( + value, + createdAt, + normalizeTimestamp(set.getInt("updated_at"), createdAt) + ); + + USER_ASSIGNMENT_CACHE.put(cacheKey, CachedUserAssignment.present(assignment)); + return assignment; + } + } catch (SQLException e) { + LOGGER.error("Failed to load shared wired user variable {} for room {} user {}", reference.getSourceVariableItemId(), reference.getSourceRoomId(), userId, e); + return null; + } + } + + public static boolean assignSharedUserVariable(WiredExtraVariableReference reference, int userId, Integer value, boolean overrideExisting) { + if (reference == null || !reference.isUserReference() || reference.isReadOnly() || userId <= 0 || !isSharedSourceStillAvailable(reference)) { + return false; + } + + Integer normalizedValue = reference.hasValue() ? value : null; + SharedUserAssignment existingAssignment = getSharedUserAssignment(reference, userId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + int now = Emulator.getIntUnixTimestamp(); + SharedUserAssignment nextAssignment = (existingAssignment == null) + ? new SharedUserAssignment(normalizedValue, now, now) + : new SharedUserAssignment(normalizedValue, existingAssignment.getCreatedAt(), Objects.equals(existingAssignment.getValue(), normalizedValue) ? existingAssignment.getUpdatedAt() : now); + + if (existingAssignment != null && Objects.equals(existingAssignment.getValue(), normalizedValue)) { + return false; + } + + upsertSharedUserAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId, nextAssignment); + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId), CachedUserAssignment.present(nextAssignment)); + return true; + } + + public static boolean updateSharedUserVariable(WiredExtraVariableReference reference, int userId, Integer value) { + if (reference == null || !reference.isUserReference() || reference.isReadOnly() || userId <= 0 || !reference.hasValue() || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedUserAssignment existingAssignment = getSharedUserAssignment(reference, userId); + if (existingAssignment == null || Objects.equals(existingAssignment.getValue(), value)) { + return false; + } + + SharedUserAssignment nextAssignment = new SharedUserAssignment(value, existingAssignment.getCreatedAt(), Emulator.getIntUnixTimestamp()); + upsertSharedUserAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId, nextAssignment); + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId), CachedUserAssignment.present(nextAssignment)); + return true; + } + + public static boolean removeSharedUserVariable(WiredExtraVariableReference reference, int userId) { + if (reference == null || !reference.isUserReference() || reference.isReadOnly() || userId <= 0 || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedUserAssignment existingAssignment = getSharedUserAssignment(reference, userId); + if (existingAssignment == null) { + return false; + } + + deleteSharedUserAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId); + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId), CachedUserAssignment.missing()); + return true; + } + + public static void cacheSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, Integer value, int createdAt, int updatedAt) { + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(sourceRoomId, sourceVariableItemId, userId), CachedUserAssignment.present(new SharedUserAssignment(value, createdAt, updatedAt))); + } + + public static void clearSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) { + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(sourceRoomId, sourceVariableItemId, userId), CachedUserAssignment.missing()); + } + + public static void clearSharedUserDefinition(int sourceRoomId, int sourceVariableItemId) { + String prefix = createDefinitionPrefix(sourceRoomId, sourceVariableItemId) + ":"; + USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix)); + } + + public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) { + if (reference == null || !reference.isRoomReference()) { + return null; + } + + String cacheKey = createRoomCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId()); + CachedRoomAssignment cachedValue = ROOM_ASSIGNMENT_CACHE.get(cacheKey); + + if (cachedValue != null) { + return cachedValue.present ? cachedValue.toAssignment() : null; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT value, updated_at FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ? LIMIT 1")) { + statement.setInt(1, reference.getSourceRoomId()); + statement.setInt(2, reference.getSourceVariableItemId()); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + ROOM_ASSIGNMENT_CACHE.put(cacheKey, CachedRoomAssignment.missing()); + return null; + } + + SharedRoomAssignment assignment = new SharedRoomAssignment(set.getInt("value"), normalizeTimestamp(set.getInt("updated_at"), 0)); + ROOM_ASSIGNMENT_CACHE.put(cacheKey, CachedRoomAssignment.present(assignment)); + return assignment; + } + } catch (SQLException e) { + LOGGER.error("Failed to load shared wired room variable {} for room {}", reference.getSourceVariableItemId(), reference.getSourceRoomId(), e); + return null; + } + } + + public static boolean updateSharedRoomVariable(WiredExtraVariableReference reference, int value) { + if (reference == null || !reference.isRoomReference() || reference.isReadOnly() || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedRoomAssignment existingAssignment = getSharedRoomAssignment(reference); + if (existingAssignment != null && existingAssignment.getValue() == value) { + return false; + } + + SharedRoomAssignment nextAssignment = new SharedRoomAssignment(value, Emulator.getIntUnixTimestamp()); + upsertSharedRoomAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), nextAssignment); + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId()), CachedRoomAssignment.present(nextAssignment)); + return true; + } + + public static boolean removeSharedRoomVariable(WiredExtraVariableReference reference) { + if (reference == null || !reference.isRoomReference() || reference.isReadOnly() || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedRoomAssignment existingAssignment = getSharedRoomAssignment(reference); + if (existingAssignment == null) { + return false; + } + + deleteSharedRoomAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId()); + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId()), CachedRoomAssignment.missing()); + return true; + } + + public static void cacheSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, int value, int updatedAt) { + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(sourceRoomId, sourceVariableItemId), CachedRoomAssignment.present(new SharedRoomAssignment(value, updatedAt))); + } + + public static void clearSharedRoomDefinition(int sourceRoomId, int sourceVariableItemId) { + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(sourceRoomId, sourceVariableItemId), CachedRoomAssignment.missing()); + } + + private static SharedDefinitionOption parseSharedDefinition(String interactionType, int itemId, String wiredData, int roomId, String roomName) { + if ("wf_var_user".equals(interactionType)) { + UserDefinitionData data = parseUserDefinitionData(wiredData); + if (data == null || !isSharedAvailability(data.availability) || data.variableName.isEmpty()) { + return null; + } + + return new SharedDefinitionOption(roomId, roomName, itemId, data.variableName, TARGET_USER, data.hasValue); + } + + if ("wf_var_room".equals(interactionType)) { + RoomDefinitionData data = parseRoomDefinitionData(wiredData); + if (data == null || !isSharedAvailability(data.availability) || data.variableName.isEmpty()) { + return null; + } + + return new SharedDefinitionOption(roomId, roomName, itemId, data.variableName, TARGET_ROOM, true); + } + + return null; + } + + private static UserDefinitionData parseUserDefinitionData(String wiredData) { + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return null; + } + + UserDefinitionData data = WiredManager.getGson().fromJson(wiredData, UserDefinitionData.class); + if (data == null) { + return null; + } + + data.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + return data; + } + + private static RoomDefinitionData parseRoomDefinitionData(String wiredData) { + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return null; + } + + RoomDefinitionData data = WiredManager.getGson().fromJson(wiredData, RoomDefinitionData.class); + if (data == null) { + return null; + } + + data.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + return data; + } + + private static boolean isSharedSourceStillAvailable(WiredExtraVariableReference reference) { + if (reference == null || reference.getSourceRoomId() <= 0 || reference.getSourceVariableItemId() <= 0) { + return false; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT items.wired_data, items_base.interaction_type " + + "FROM items INNER JOIN items_base ON items.item_id = items_base.id " + + "WHERE items.id = ? AND items.room_id = ? LIMIT 1")) { + statement.setInt(1, reference.getSourceVariableItemId()); + statement.setInt(2, reference.getSourceRoomId()); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + return false; + } + + SharedDefinitionOption definition = parseSharedDefinition( + set.getString("interaction_type"), + reference.getSourceVariableItemId(), + set.getString("wired_data"), + reference.getSourceRoomId(), + "" + ); + + return definition != null && definition.getTargetType() == reference.getSourceTargetType(); + } + } catch (SQLException e) { + LOGGER.error("Failed to validate shared wired variable source {} in room {}", reference.getSourceVariableItemId(), reference.getSourceRoomId(), e); + return false; + } + } + + private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, userId); + statement.setInt(3, sourceVariableItemId); + + if (assignment.getValue() == null) { + statement.setNull(4, java.sql.Types.INTEGER); + } else { + statement.setInt(4, assignment.getValue()); + } + + statement.setInt(5, assignment.getCreatedAt()); + statement.setInt(6, assignment.getUpdatedAt()); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e); + } + } + + private static void deleteSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, userId); + statement.setInt(3, sourceVariableItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e); + } + } + + private static void upsertSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, SharedRoomAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, sourceVariableItemId); + statement.setInt(3, assignment.getValue()); + statement.setInt(4, 0); + statement.setInt(5, assignment.getUpdatedAt()); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e); + } + } + + private static void deleteSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, sourceVariableItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e); + } + } + + private static String createDefinitionPrefix(int sourceRoomId, int sourceVariableItemId) { + return sourceRoomId + ":" + sourceVariableItemId; + } + + private static String createUserCacheKey(int sourceRoomId, int sourceVariableItemId, int userId) { + return createDefinitionPrefix(sourceRoomId, sourceVariableItemId) + ":" + userId; + } + + private static String createRoomCacheKey(int sourceRoomId, int sourceVariableItemId) { + return createDefinitionPrefix(sourceRoomId, sourceVariableItemId); + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) { + return value; + } + + if (fallback > 0) { + return fallback; + } + + return Emulator.getIntUnixTimestamp(); + } + + public static class RoomOption { + private final int roomId; + private final String roomName; + private final List variables; + + public RoomOption(int roomId, String roomName, List variables) { + this.roomId = roomId; + this.roomName = roomName; + this.variables = variables; + } + + public int getRoomId() { + return this.roomId; + } + + public String getRoomName() { + return this.roomName; + } + + public List getVariables() { + return this.variables; + } + } + + public static class SharedDefinitionOption { + private final int roomId; + private final String roomName; + private final int itemId; + private final String name; + private final int targetType; + private final boolean hasValue; + + public SharedDefinitionOption(int roomId, String roomName, int itemId, String name, int targetType, boolean hasValue) { + this.roomId = roomId; + this.roomName = roomName; + this.itemId = itemId; + this.name = name; + this.targetType = targetType; + this.hasValue = hasValue; + } + + public int getRoomId() { + return this.roomId; + } + + public String getRoomName() { + return this.roomName; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public int getTargetType() { + return this.targetType; + } + + public boolean hasValue() { + return this.hasValue; + } + } + + public static class SharedUserAssignment { + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public SharedUserAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return this.value; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + public static class SharedRoomAssignment { + private final int value; + private final int updatedAt; + + public SharedRoomAssignment(int value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getValue() { + return this.value; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static class CachedUserAssignment { + private final boolean present; + private final SharedUserAssignment assignment; + + private CachedUserAssignment(boolean present, SharedUserAssignment assignment) { + this.present = present; + this.assignment = assignment; + } + + private static CachedUserAssignment present(SharedUserAssignment assignment) { + return new CachedUserAssignment(true, assignment); + } + + private static CachedUserAssignment missing() { + return new CachedUserAssignment(false, null); + } + + private SharedUserAssignment toAssignment() { + return this.assignment; + } + } + + private static class CachedRoomAssignment { + private final boolean present; + private final SharedRoomAssignment assignment; + + private CachedRoomAssignment(boolean present, SharedRoomAssignment assignment) { + this.present = present; + this.assignment = assignment; + } + + private static CachedRoomAssignment present(SharedRoomAssignment assignment) { + return new CachedRoomAssignment(true, assignment); + } + + private static CachedRoomAssignment missing() { + return new CachedRoomAssignment(false, null); + } + + private SharedRoomAssignment toAssignment() { + return this.assignment; + } + } + + private static class UserDefinitionData { + String variableName; + boolean hasValue; + int availability; + } + + private static class RoomDefinitionData { + String variableName; + int availability; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniWithVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniWithVariable.java new file mode 100644 index 00000000..e3d4032c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniWithVariable.java @@ -0,0 +1,29 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectFurniWithVariable extends WiredEffectVariableSelectorBase { + public static final WiredEffectType type = WiredEffectType.FURNI_WITH_VAR_SELECTOR; + + public WiredEffectFurniWithVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniWithVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_FURNI; + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersWithVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersWithVariable.java new file mode 100644 index 00000000..1046ad42 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersWithVariable.java @@ -0,0 +1,29 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectUsersWithVariable extends WiredEffectVariableSelectorBase { + public static final WiredEffectType type = WiredEffectType.USERS_WITH_VAR_SELECTOR; + + public WiredEffectUsersWithVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersWithVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_USER; + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java new file mode 100644 index 00000000..11916849 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java @@ -0,0 +1,862 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWired; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.util.HotelDateTimeUtil; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEffect { + protected static final int TARGET_USER = 0; + protected static final int TARGET_FURNI = 1; + protected static final int TARGET_CONTEXT = 2; + protected static final int TARGET_ROOM = 3; + + protected static final int REFERENCE_CONSTANT = 0; + protected static final int REFERENCE_VARIABLE = 1; + + protected static final int SOURCE_SECONDARY_SELECTED = 101; + + protected static final int COMPARISON_GREATER_THAN = 0; + protected static final int COMPARISON_GREATER_THAN_OR_EQUAL = 1; + protected static final int COMPARISON_EQUAL = 2; + protected static final int COMPARISON_LESS_THAN_OR_EQUAL = 3; + protected static final int COMPARISON_LESS_THAN = 4; + protected static final int COMPARISON_NOT_EQUAL = 5; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final String DELIM = "\t"; + + protected boolean selectByValue = false; + protected int comparison = COMPARISON_EQUAL; + protected int referenceMode = REFERENCE_CONSTANT; + protected int referenceConstantValue = 0; + protected int referenceTargetType = TARGET_USER; + protected int referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected boolean filterExisting = false; + protected boolean invert = false; + protected String variableToken = ""; + protected int variableItemId = 0; + protected String referenceVariableToken = ""; + protected int referenceVariableItemId = 0; + protected final THashSet referenceSelectedItems = new THashSet<>(); + + protected WiredEffectVariableSelectorBase(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + protected WiredEffectVariableSelectorBase(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + protected abstract int getVariableTargetType(); + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { + return; + } + + if (this.getVariableTargetType() == TARGET_FURNI) { + LinkedHashSet matchedItems = new LinkedHashSet<>(); + + for (HabboItem item : this.getSelectableFloorItems(room, ctx)) { + if (item == null) continue; + if (!this.matchesFurni(room, item, ctx)) continue; + + matchedItems.add(item); + } + + LinkedHashSet result = this.applySelectorModifiers(matchedItems, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), this.filterExisting, this.invert); + ctx.targets().setItems(result); + return; + } + + LinkedHashSet matchedUsers = new LinkedHashSet<>(); + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (roomUnit == null) continue; + if (!this.matchesUser(room, roomUnit, ctx)) continue; + + matchedUsers.add(roomUnit); + } + + LinkedHashSet result = this.applySelectorModifiers(matchedUsers, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = this.getRoom(); + if (room == null) return false; + + int[] params = settings.getIntParams(); + String[] stringParts = parseStringData(settings.getStringParam()); + + boolean nextSelectByValue = param(params, 0, 0) == 1; + int nextComparison = normalizeComparison(param(params, 1, COMPARISON_EQUAL)); + int nextReferenceMode = normalizeReferenceMode(param(params, 2, REFERENCE_CONSTANT)); + int nextReferenceConstantValue = param(params, 3, 0); + int nextReferenceTargetType = normalizeReferenceTargetType(param(params, 4, TARGET_USER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); + boolean nextFilterExisting = param(params, 7, 0) == 1; + boolean nextInvert = param(params, 8, 0) == 1; + String nextVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : settings.getStringParam()); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + + if (!this.isValidMainVariable(room, nextVariableToken, nextSelectByValue)) return false; + if (nextSelectByValue && nextReferenceMode == REFERENCE_VARIABLE && !this.isValidReference(room, nextReferenceTargetType, nextReferenceVariableToken)) return false; + + List nextReferenceItems = new ArrayList<>(); + + if (nextSelectByValue && nextReferenceMode == REFERENCE_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) { + int[] furniIds = settings.getFurniIds(); + if (furniIds.length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; + + for (int furniId : furniIds) { + HabboItem item = room.getHabboItem(furniId); + if (item != null) nextReferenceItems.add(item); + } + } + + this.referenceSelectedItems.clear(); + this.referenceSelectedItems.addAll(nextReferenceItems); + this.selectByValue = nextSelectByValue; + this.comparison = nextComparison; + this.referenceMode = nextReferenceMode; + this.referenceConstantValue = nextReferenceConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.filterExisting = nextFilterExisting; + this.invert = nextInvert; + this.setVariableToken(nextVariableToken); + this.setReferenceVariableToken(nextReferenceVariableToken); + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + this.refreshReferenceItems(); + + return WiredManager.getGson().toJson(new JsonData( + this.selectByValue, + this.comparison, + this.referenceMode, + this.referenceConstantValue, + this.referenceTargetType, + this.referenceUserSource, + this.referenceFurniSource, + this.filterExisting, + this.invert, + this.variableToken, + this.variableItemId, + this.referenceVariableToken, + this.referenceVariableItemId, + this.toIds(this.referenceSelectedItems), + this.getDelay() + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.selectByValue = data.selectByValue; + this.comparison = normalizeComparison(data.comparison); + this.referenceMode = normalizeReferenceMode(data.referenceMode); + this.referenceConstantValue = data.referenceConstantValue; + this.referenceTargetType = normalizeReferenceTargetType(data.referenceTargetType); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + this.setDelay(data.delay); + + if (room == null || data.selectedItemIds == null) return; + + for (Integer itemId : data.selectedItemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.referenceSelectedItems.add(item); + } + } + + @Override + public void onPickUp() { + this.selectByValue = false; + this.comparison = COMPARISON_EQUAL; + this.referenceMode = REFERENCE_CONSTANT; + this.referenceConstantValue = 0; + this.referenceTargetType = TARGET_USER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.filterExisting = false; + this.invert = false; + this.referenceSelectedItems.clear(); + this.setVariableToken(""); + this.setReferenceVariableToken(""); + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refreshReferenceItems(); + + List serializedItems = new ArrayList<>(); + if (this.selectByValue && this.referenceMode == REFERENCE_VARIABLE && this.referenceTargetType == TARGET_FURNI && this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) { + serializedItems.addAll(this.referenceSelectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(9); + message.appendInt(this.selectByValue ? 1 : 0); + message.appendInt(this.comparison); + message.appendInt(this.referenceMode); + message.appendInt(this.referenceConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean requiresTriggeringUser() { + return this.selectByValue && this.referenceMode == REFERENCE_VARIABLE && this.referenceTargetType == TARGET_USER && this.referenceUserSource == WiredSourceUtil.SOURCE_TRIGGER; + } + + private boolean matchesUser(Room room, RoomUnit roomUnit, WiredContext ctx) { + if (!this.selectByValue) return this.hasUserVariable(room, roomUnit); + + Integer currentValue = this.readUserValue(room, roomUnit); + Integer referenceValue = this.resolveReferenceValue(ctx, room, roomUnit != null ? roomUnit.getId() : 0, TARGET_USER, -1); + + return this.matchesComparison(currentValue, referenceValue); + } + + private boolean matchesFurni(Room room, HabboItem item, WiredContext ctx) { + if (!this.selectByValue) return this.hasFurniVariable(room, item); + + Integer currentValue = this.readFurniValue(room, item); + Integer referenceValue = this.resolveReferenceValue(ctx, room, item != null ? item.getId() : 0, TARGET_FURNI, -1); + + return this.matchesComparison(currentValue, referenceValue); + } + + private boolean hasUserVariable(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return false; + + if (isCustomVariableToken(this.variableToken)) { + Habbo habbo = room.getHabbo(roomUnit); + return habbo != null && room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId); + } + + return isInternalVariableToken(this.variableToken) && this.hasUserInternalVariable(room, roomUnit, getInternalVariableKey(this.variableToken)); + } + + private boolean hasFurniVariable(Room room, HabboItem item) { + if (room == null || item == null) return false; + + if (isCustomVariableToken(this.variableToken)) { + return room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId); + } + + return isInternalVariableToken(this.variableToken) && this.hasFurniInternalVariable(item, getInternalVariableKey(this.variableToken)); + } + + private Integer resolveReferenceValue(WiredContext ctx, Room room, int destinationEntityId, int destinationTargetType, int destinationIndex) { + if (!this.selectByValue || this.referenceMode != REFERENCE_VARIABLE) return this.referenceConstantValue; + + ReferenceSnapshot snapshot = this.resolveReferences(ctx, room); + if (snapshot == null || snapshot.isEmpty()) return null; + if (snapshot.targetType == destinationTargetType && snapshot.values.containsKey(destinationEntityId)) return snapshot.values.get(destinationEntityId); + if (destinationIndex >= 0 && destinationIndex < snapshot.values.size()) return new ArrayList<>(snapshot.values.values()).get(destinationIndex); + + return new ArrayList<>(snapshot.values.values()).get(0); + } + + private ReferenceSnapshot resolveReferences(WiredContext ctx, Room room) { + return switch (this.referenceTargetType) { + case TARGET_FURNI -> this.furniReferences(ctx, room); + case TARGET_CONTEXT -> this.contextReferences(ctx, room); + case TARGET_ROOM -> this.roomReferences(room); + default -> this.userReferences(ctx, room); + }; + } + + private ReferenceSnapshot userReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_USER); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseUserInternalReference(key)) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + Integer value = this.readUserInternalValue(room, roomUnit, key); + if (value != null && roomUnit != null) snapshot.add(roomUnit.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) snapshot.add(roomUnit.getId(), room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot furniReferences(WiredContext ctx, Room room) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.refreshReferenceItems(); + + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_FURNI); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseFurniInternalReference(key)) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + Integer value = this.readFurniInternalValue(room, item, key); + if (value != null && item != null) snapshot.add(item.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + if (item != null) snapshot.add(item.getId(), room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot roomReferences(Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_ROOM); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseRoomInternalReference(key)) return null; + + Integer value = this.readRoomInternalValue(room, key); + if (value == null) return null; + + snapshot.add(room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + snapshot.add(room.getId(), room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId)); + return snapshot; + } + + private ReferenceSnapshot contextReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_CONTEXT); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseContextInternalReference(key)) return null; + + Integer value = WiredInternalVariableSupport.readContextValue(ctx, key); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId > 0 ? this.referenceVariableItemId : (room != null ? room.getId() : 0), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId, value); + return snapshot; + } + + private boolean isValidMainVariable(Room room, String token, boolean requireValue) { + if (token == null || token.isEmpty()) return false; + + int targetType = this.getVariableTargetType(); + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return targetType == TARGET_FURNI + ? (requireValue ? canUseFurniInternalReference(key) : this.hasFurniInternalKey(key)) + : (requireValue ? canUseUserInternalReference(key) : this.hasUserInternalKey(key)); + } + + if (targetType == TARGET_FURNI) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + return definition != null && (!requireValue || definition.hasValue()); + } + + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + return definition != null && (!requireValue || definition.hasValue()); + } + + private boolean isValidReference(Room room, int targetType, String token) { + if (token == null || token.isEmpty()) return false; + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return switch (targetType) { + case TARGET_FURNI -> canUseFurniInternalReference(key); + case TARGET_CONTEXT -> canUseContextInternalReference(key); + case TARGET_ROOM -> canUseRoomInternalReference(key); + default -> canUseUserInternalReference(key); + }; + } + + return switch (targetType) { + case TARGET_FURNI -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + case TARGET_CONTEXT -> this.isValidContextCustomReference(room, getCustomItemId(token)); + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + default -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + }; + } + + private boolean isValidContextCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + + private boolean matchesComparison(Integer currentValue, Integer referenceValue) { + if (currentValue == null || referenceValue == null) return false; + + return switch (this.comparison) { + case COMPARISON_GREATER_THAN -> currentValue > referenceValue; + case COMPARISON_GREATER_THAN_OR_EQUAL -> currentValue >= referenceValue; + case COMPARISON_LESS_THAN_OR_EQUAL -> currentValue <= referenceValue; + case COMPARISON_LESS_THAN -> currentValue < referenceValue; + case COMPARISON_NOT_EQUAL -> !currentValue.equals(referenceValue); + default -> currentValue.equals(referenceValue); + }; + } + + private Integer readUserValue(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + if (definition == null || !definition.hasValue()) return null; + + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null) ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.variableItemId) : null; + } + + private Integer readFurniValue(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + return (definition != null && definition.hasValue()) ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.variableItemId) : null; + } + + private boolean hasUserInternalVariable(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.hasUserValue(room, roomUnit, key); + } + + private boolean hasFurniInternalVariable(HabboItem item, String key) { + return WiredInternalVariableSupport.hasFurniValue(item, key); + } + + private boolean hasUserInternalKey(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private boolean hasFurniInternalKey(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key) || "@wallitem_offset".equals(WiredInternalVariableSupport.normalizeKey(key)); + } + + private boolean hasRoomEntryMethod(Habbo habbo) { + if (habbo == null) return false; + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + return roomEntryMethod != null && !roomEntryMethod.trim().isEmpty() && !"unknown".equalsIgnoreCase(roomEntryMethod); + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private Integer getTeamColorId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private Integer getTeamTypeId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + private void refreshReferenceItems() { + THashSet staleItems = new THashSet<>(); + Room room = this.getRoom(); + + if (room == null) { + staleItems.addAll(this.referenceSelectedItems); + } else { + for (HabboItem item : this.referenceSelectedItems) { + if (item == null || item.getRoomId() != room.getId() || room.getHabboItem(item.getId()) == null) { + staleItems.add(item); + } + } + } + + this.referenceSelectedItems.removeAll(staleItems); + } + + private String serializeStringData() { + return (this.variableToken == null ? "" : this.variableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken); + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private List toIds(THashSet items) { + List ids = new ArrayList<>(); + + for (HabboItem item : items) { + if (item != null) ids.add(item.getId()); + } + + return ids; + } + + private static int normalizeReferenceMode(int value) { + return (value == REFERENCE_VARIABLE) ? REFERENCE_VARIABLE : REFERENCE_CONSTANT; + } + + private static int normalizeReferenceTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeComparison(int value) { + return switch (value) { + case COMPARISON_GREATER_THAN, COMPARISON_GREATER_THAN_OR_EQUAL, COMPARISON_LESS_THAN_OR_EQUAL, COMPARISON_LESS_THAN, COMPARISON_NOT_EQUAL -> value; + default -> COMPARISON_EQUAL; + }; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + protected static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + protected static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + private static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException e) { + return 0; + } + } + + private static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + protected static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (isCustomVariableToken(normalized)) return normalized; + if (isInternalVariableToken(normalized)) return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + + try { + int parsed = Integer.parseInt(normalized); + return (parsed > 0) ? (CUSTOM_TOKEN_PREFIX + parsed) : ""; + } catch (NumberFormatException e) { + return ""; + } + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params != null && params.length > index) ? params[index] : fallback; + } + + private static String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + protected static class JsonData { + boolean selectByValue; + int comparison; + int referenceMode; + int referenceConstantValue; + int referenceTargetType; + int referenceUserSource; + int referenceFurniSource; + boolean filterExisting; + boolean invert; + String variableToken; + int variableItemId; + String referenceVariableToken; + int referenceVariableItemId; + List selectedItemIds; + int delay; + + JsonData(boolean selectByValue, int comparison, int referenceMode, int referenceConstantValue, int referenceTargetType, int referenceUserSource, int referenceFurniSource, boolean filterExisting, boolean invert, String variableToken, int variableItemId, String referenceVariableToken, int referenceVariableItemId, List selectedItemIds, int delay) { + this.selectByValue = selectByValue; + this.comparison = comparison; + this.referenceMode = referenceMode; + this.referenceConstantValue = referenceConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.filterExisting = filterExisting; + this.invert = invert; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.selectedItemIds = selectedItemIds; + this.delay = delay; + } + } + + private static class ReferenceSnapshot { + final int targetType; + final LinkedHashMap values = new LinkedHashMap<>(); + + ReferenceSnapshot(int targetType) { + this.targetType = targetType; + } + + void add(int entityId, int value) { + this.values.put(entityId, value); + } + + boolean isEmpty() { + return this.values.isEmpty(); + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java index 2585a816..26d43d33 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java @@ -141,6 +141,18 @@ public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { return this.hideMessage; } + public boolean isOwnerOnly() { + return this.ownerOnly; + } + + public String getKey() { + return this.key; + } + + public int getMatchMode() { + return this.matchMode; + } + private boolean matchesText(String text) { String normalizedText = text.toLowerCase().trim(); String normalizedKey = this.key.toLowerCase().trim(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerVariableChanged.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerVariableChanged.java new file mode 100644 index 00000000..c638c12f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerVariableChanged.java @@ -0,0 +1,302 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredTriggerVariableChanged extends InteractionWiredTrigger { + public static final WiredTriggerType type = WiredTriggerType.VARIABLE_CHANGED; + + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_ROOM = 3; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + + private String variableToken = ""; + private int variableItemId = 0; + private int targetType = TARGET_USER; + private boolean createdEnabled = true; + private boolean valueChangedEnabled = true; + private boolean increasedEnabled = true; + private boolean decreasedEnabled = true; + private boolean unchangedEnabled = true; + private boolean deletedEnabled = true; + + public WiredTriggerVariableChanged(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredTriggerVariableChanged(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + if (event == null || event.getType() != WiredEvent.Type.VARIABLE_CHANGED) { + return false; + } + + if (event.getVariableTargetType() != this.targetType || event.getVariableDefinitionItemId() != this.variableItemId) { + return false; + } + + if (this.createdEnabled && event.isVariableCreated()) { + return true; + } + + if (this.deletedEnabled && event.isVariableDeleted()) { + return true; + } + + if (!this.valueChangedEnabled) { + return false; + } + + return switch (event.getVariableChangeKind()) { + case INCREASED -> this.increasedEnabled; + case DECREASED -> this.decreasedEnabled; + case UNCHANGED -> this.unchangedEnabled; + default -> false; + }; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken == null ? "" : this.variableToken); + message.appendInt(7); + message.appendInt(this.targetType); + message.appendInt(this.createdEnabled ? 1 : 0); + message.appendInt(this.valueChangedEnabled ? 1 : 0); + message.appendInt(this.increasedEnabled ? 1 : 0); + message.appendInt(this.decreasedEnabled ? 1 : 0); + message.appendInt(this.unchangedEnabled ? 1 : 0); + message.appendInt(this.deletedEnabled ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + int[] params = settings.getIntParams(); + + this.targetType = normalizeTargetType((params.length > 0) ? params[0] : TARGET_USER); + this.createdEnabled = (params.length <= 1) || (params[1] == 1); + this.valueChangedEnabled = (params.length <= 2) || (params[2] == 1); + this.increasedEnabled = (params.length <= 3) || (params[3] == 1); + this.decreasedEnabled = (params.length <= 4) || (params[4] == 1); + this.unchangedEnabled = (params.length <= 5) || (params[5] == 1); + this.deletedEnabled = (params.length <= 6) || (params[6] == 1); + this.setVariableToken(normalizeVariableToken(settings.getStringParam())); + this.normalizeOptions(); + + if (this.variableItemId <= 0) { + throw new WiredTriggerSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + if (!this.hasAnyEnabledOption()) { + return false; + } + + if (room == null || !this.isValidDefinition(room)) { + throw new WiredTriggerSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.variableToken, + this.variableItemId, + this.targetType, + this.createdEnabled, + this.valueChangedEnabled, + this.increasedEnabled, + this.decreasedEnabled, + this.unchangedEnabled, + this.deletedEnabled + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (!wiredData.startsWith("{")) { + this.setVariableToken(normalizeVariableToken(wiredData)); + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.targetType = normalizeTargetType(data.targetType); + this.createdEnabled = data.createdEnabled; + this.valueChangedEnabled = data.valueChangedEnabled; + this.increasedEnabled = data.increasedEnabled; + this.decreasedEnabled = data.decreasedEnabled; + this.unchangedEnabled = data.unchangedEnabled; + this.deletedEnabled = data.deletedEnabled; + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.normalizeOptions(); + } + + @Override + public void onPickUp() { + this.variableToken = ""; + this.variableItemId = 0; + this.targetType = TARGET_USER; + this.createdEnabled = true; + this.valueChangedEnabled = true; + this.increasedEnabled = true; + this.decreasedEnabled = true; + this.unchangedEnabled = true; + this.deletedEnabled = true; + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private void normalizeOptions() { + if (!this.valueChangedEnabled) { + this.increasedEnabled = false; + this.decreasedEnabled = false; + this.unchangedEnabled = false; + } + + if (this.targetType == TARGET_ROOM) { + this.createdEnabled = false; + this.deletedEnabled = false; + } + } + + private boolean hasAnyEnabledOption() { + return this.createdEnabled + || this.deletedEnabled + || (this.valueChangedEnabled && (this.increasedEnabled || this.decreasedEnabled || this.unchangedEnabled)); + } + + private boolean isValidDefinition(Room room) { + WiredVariableDefinitionInfo definitionInfo = switch (this.targetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getDefinitionInfo(this.variableItemId); + default -> room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + }; + + return definitionInfo != null; + } + + private static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static String normalizeVariableToken(String token) { + if (token == null) { + return ""; + } + + String normalized = token.trim(); + if (normalized.isEmpty()) { + return ""; + } + + if (normalized.startsWith(CUSTOM_TOKEN_PREFIX)) { + return normalized; + } + + try { + int itemId = Integer.parseInt(normalized); + return (itemId > 0) ? (CUSTOM_TOKEN_PREFIX + itemId) : ""; + } catch (NumberFormatException ignored) { + return ""; + } + } + + private static int getCustomItemId(String token) { + if (token == null || !token.startsWith(CUSTOM_TOKEN_PREFIX)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + String variableToken; + int variableItemId; + int targetType; + boolean createdEnabled; + boolean valueChangedEnabled; + boolean increasedEnabled; + boolean decreasedEnabled; + boolean unchangedEnabled; + boolean deletedEnabled; + + JsonData(String variableToken, int variableItemId, int targetType, boolean createdEnabled, boolean valueChangedEnabled, boolean increasedEnabled, boolean decreasedEnabled, boolean unchangedEnabled, boolean deletedEnabled) { + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.targetType = targetType; + this.createdEnabled = createdEnabled; + this.valueChangedEnabled = valueChangedEnabled; + this.increasedEnabled = increasedEnabled; + this.decreasedEnabled = decreasedEnabled; + this.unchangedEnabled = unchangedEnabled; + this.deletedEnabled = deletedEnabled; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java index 0227d0b3..1984ad28 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java @@ -54,11 +54,23 @@ public class ModToolManager { if (userId <= 0) return; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.*, users_settings.*, permissions.rank_name, permissions.acc_hide_mail AS hide_mail, permissions.id AS rank_id FROM users INNER JOIN users_settings ON users.id = users_settings.user_id INNER JOIN permissions ON permissions.id = users.rank WHERE users.id = ? LIMIT 1")) { + String query = Emulator.getGameEnvironment().getPermissionsManager().isNormalizedSchemaEnabled() + ? "SELECT users.*, users_settings.*, permission_ranks.rank_name, permission_ranks.id AS rank_id " + + "FROM users " + + "INNER JOIN users_settings ON users.id = users_settings.user_id " + + "INNER JOIN permission_ranks ON permission_ranks.id = users.rank " + + "WHERE users.id = ? LIMIT 1" + : "SELECT users.*, users_settings.*, permissions.rank_name, permissions.acc_hide_mail AS hide_mail, permissions.id AS rank_id FROM users INNER JOIN users_settings ON users.id = users_settings.user_id INNER JOIN permissions ON permissions.id = users.rank WHERE users.id = ? LIMIT 1"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) { statement.setInt(1, userId); try (ResultSet set = statement.executeQuery()) { while (set.next()) { - client.sendResponse(new ModToolUserInfoComposer(set)); + boolean hideMail = Emulator.getGameEnvironment().getPermissionsManager().isNormalizedSchemaEnabled() + ? Emulator.getGameEnvironment().getPermissionsManager().getRank(set.getInt("rank_id")).hasPermission("acc_hide_mail", false) + : set.getBoolean("hide_mail"); + + client.sendResponse(new ModToolUserInfoComposer(set, hideMail)); } } } catch (SQLException e) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java index b9d24931..0286a468 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import java.sql.*; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -20,6 +21,7 @@ public class PermissionsManager { private final TIntObjectHashMap ranks; private final TIntIntHashMap enables; private final THashMap> badges; + private volatile boolean normalizedSchemaEnabled; public PermissionsManager() { long millis = System.currentTimeMillis(); @@ -40,7 +42,30 @@ public class PermissionsManager { private void loadPermissions() { this.badges.clear(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permissions ORDER BY id ASC")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + if (this.hasNormalizedPermissionsSchema(connection)) { + try { + if (this.loadPermissionsNormalized(connection)) { + this.normalizedSchemaEnabled = true; + LOGGER.info("Permissions Manager -> Using normalized permissions schema."); + return; + } + } catch (SQLException e) { + LOGGER.warn("Permissions Manager -> Failed to load normalized permissions schema, falling back to legacy permissions table.", e); + } + } + + this.normalizedSchemaEnabled = false; + this.badges.clear(); + LOGGER.info("Permissions Manager -> Using legacy permissions schema."); + this.loadPermissionsLegacy(connection); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + private void loadPermissionsLegacy(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permissions ORDER BY id ASC")) { while (set.next()) { Rank rank = null; if (!this.ranks.containsKey(set.getInt("id"))) { @@ -51,16 +76,135 @@ public class PermissionsManager { rank.load(set); } - if (rank != null && !rank.getBadge().isEmpty()) { - if (!this.badges.containsKey(rank.getBadge())) { - this.badges.put(rank.getBadge(), new ArrayList()); - } + this.addBadgeMapping(rank); + } + } + } - this.badges.get(rank.getBadge()).add(rank); + private boolean loadPermissionsNormalized(Connection connection) throws SQLException { + boolean hasRanks = false; + List loadedRanks = new ArrayList<>(); + + try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permission_ranks ORDER BY id ASC")) { + while (set.next()) { + hasRanks = true; + + Rank rank = this.ranks.get(set.getInt("id")); + + if (rank == null) { + rank = new Rank(set.getInt("id")); + this.ranks.put(set.getInt("id"), rank); + } + + rank.loadNormalizedMetadata(set); + this.addBadgeMapping(rank); + loadedRanks.add(rank); + } + } + + if (!hasRanks) { + return false; + } + + this.ensureNormalizedRankColumns(connection, loadedRanks); + + boolean hasDefinitions = false; + + try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM permission_definitions ORDER BY permission_key ASC"); + ResultSet set = statement.executeQuery()) { + ResultSetMetaData meta = set.getMetaData(); + Set availableColumns = new HashSet<>(); + + for (int i = 1; i <= meta.getColumnCount(); i++) { + availableColumns.add(meta.getColumnName(i).toLowerCase()); + } + + for (Rank rank : loadedRanks) { + if (!availableColumns.contains(("rank_" + rank.getId()).toLowerCase())) { + return false; } } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); + + while (set.next()) { + hasDefinitions = true; + String permissionKey = set.getString("permission_key"); + + for (Rank rank : loadedRanks) { + String rankColumn = "rank_" + rank.getId(); + + if (!availableColumns.contains(rankColumn.toLowerCase())) { + continue; + } + + rank.setPermission(permissionKey, PermissionSetting.fromString(Integer.toString(set.getInt(rankColumn)))); + } + } + } + + return hasDefinitions; + } + + private void ensureNormalizedRankColumns(Connection connection, List loadedRanks) throws SQLException { + Set availableColumns = new HashSet<>(); + + try (PreparedStatement statement = connection.prepareStatement("SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'permission_definitions'"); + ResultSet set = statement.executeQuery()) { + while (set.next()) { + availableColumns.add(set.getString("column_name").toLowerCase()); + } + } + + for (Rank rank : loadedRanks) { + String rankColumn = "rank_" + rank.getId(); + + if (availableColumns.contains(rankColumn.toLowerCase())) { + continue; + } + + try (Statement statement = connection.createStatement()) { + statement.execute("ALTER TABLE permission_definitions ADD COLUMN `" + rankColumn + "` tinyint(3) unsigned NOT NULL DEFAULT 0"); + } + + availableColumns.add(rankColumn.toLowerCase()); + LOGGER.info("Permissions Manager -> Added missing normalized permission column {}.", rankColumn); + } + } + + private boolean hasNormalizedPermissionsSchema(Connection connection) throws SQLException { + if (!this.tableExists(connection, "permission_ranks") || !this.tableExists(connection, "permission_definitions")) { + return false; + } + + if (!this.tableHasRows(connection, "permission_ranks")) { + return false; + } + + return this.tableHasRows(connection, "permission_definitions"); + } + + private boolean tableExists(Connection connection, String tableName) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?")) { + statement.setString(1, tableName); + + try (ResultSet set = statement.executeQuery()) { + return set.next() && set.getInt(1) > 0; + } + } + } + + private boolean tableHasRows(Connection connection, String tableName) throws SQLException { + try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + return set.next() && set.getInt(1) > 0; + } + } + + private void addBadgeMapping(Rank rank) { + if (rank != null && !rank.getBadge().isEmpty()) { + if (!this.badges.containsKey(rank.getBadge())) { + this.badges.put(rank.getBadge(), new ArrayList()); + } + + this.badges.get(rank.getBadge()).add(rank); } } @@ -139,4 +283,8 @@ public class PermissionsManager { public List getAllRanks() { return new ArrayList<>(this.ranks.valueCollection()); } + + public boolean isNormalizedSchemaEnabled() { + return this.normalizedSchemaEnabled; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java index cbbc01dc..897e5b5b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java @@ -35,32 +35,29 @@ public class Rank { private int gotwTimerAmount; public Rank(ResultSet set) throws SQLException { + this(set.getInt("id")); + this.load(set); + } + + public Rank(int id) { this.permissions = new THashMap<>(); this.variables = new THashMap<>(); - this.id = set.getInt("id"); - this.level = set.getInt("level"); + this.id = id; + this.level = 1; this.diamondsTimerAmount = 1; this.creditsTimerAmount = 1; this.pixelsTimerAmount = 1; this.gotwTimerAmount = 1; - - this.load(set); } public void load(ResultSet set) throws SQLException { + this.permissions.clear(); + this.variables.clear(); + + this.loadMetadata(set); + ResultSetMetaData meta = set.getMetaData(); - this.name = set.getString("rank_name"); - this.badge = set.getString("badge"); - this.roomEffect = set.getInt("room_effect"); - this.logCommands = set.getString("log_commands").equals("1"); - this.prefix = set.getString("prefix"); - this.prefixColor = set.getString("prefix_color"); - this.level = set.getInt("level"); - this.diamondsTimerAmount = set.getInt("auto_points_amount"); - this.creditsTimerAmount = set.getInt("auto_credits_amount"); - this.pixelsTimerAmount = set.getInt("auto_pixels_amount"); - this.gotwTimerAmount = set.getInt("auto_gotw_amount"); - this.hasPrefix = !this.prefix.isEmpty(); + for (int i = 1; i < meta.getColumnCount() + 1; i++) { String columnName = meta.getColumnName(i); if (columnName.startsWith("cmd_") || columnName.startsWith("acc_")) { @@ -71,6 +68,51 @@ public class Rank { } } + public void loadNormalizedMetadata(ResultSet set) throws SQLException { + this.permissions.clear(); + this.variables.clear(); + this.loadMetadata(set); + this.storeMetadataVariables(); + } + + public void setPermission(String key, PermissionSetting setting) { + this.permissions.put(key, new Permission(key, setting)); + } + + private void loadMetadata(ResultSet set) throws SQLException { + this.name = this.safeString(set.getString("rank_name")); + this.badge = this.safeString(set.getString("badge")); + this.roomEffect = set.getInt("room_effect"); + this.logCommands = "1".equals(this.safeString(set.getString("log_commands"))); + this.prefix = this.safeString(set.getString("prefix")); + this.prefixColor = this.safeString(set.getString("prefix_color")); + this.level = set.getInt("level"); + this.diamondsTimerAmount = set.getInt("auto_points_amount"); + this.creditsTimerAmount = set.getInt("auto_credits_amount"); + this.pixelsTimerAmount = set.getInt("auto_pixels_amount"); + this.gotwTimerAmount = set.getInt("auto_gotw_amount"); + this.hasPrefix = !this.prefix.isEmpty(); + } + + private void storeMetadataVariables() { + this.variables.put("id", Integer.toString(this.id)); + this.variables.put("rank_name", this.name); + this.variables.put("badge", this.badge); + this.variables.put("room_effect", Integer.toString(this.roomEffect)); + this.variables.put("log_commands", this.logCommands ? "1" : "0"); + this.variables.put("prefix", this.prefix); + this.variables.put("prefix_color", this.prefixColor); + this.variables.put("level", Integer.toString(this.level)); + this.variables.put("auto_points_amount", Integer.toString(this.diamondsTimerAmount)); + this.variables.put("auto_credits_amount", Integer.toString(this.creditsTimerAmount)); + this.variables.put("auto_pixels_amount", Integer.toString(this.pixelsTimerAmount)); + this.variables.put("auto_gotw_amount", Integer.toString(this.gotwTimerAmount)); + } + + private String safeString(String value) { + return value == null ? "" : value; + } + public boolean hasPermission(String key, boolean isRoomOwner) { if (this.permissions.containsKey(key)) { Permission permission = this.permissions.get(key); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index c4d9e653..d0231b6f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.games.Game; import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.guilds.GuildMember; +import com.eu.habbo.habbohotel.guilds.GuildRank; import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.*; @@ -30,6 +31,7 @@ import com.eu.habbo.messages.outgoing.rooms.UpdateStackHeightComposer; import com.eu.habbo.messages.outgoing.rooms.items.*; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserIgnoredComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; +import com.eu.habbo.messages.outgoing.wired.WiredRoomSettingsDataComposer; import com.eu.habbo.plugin.Event; import com.eu.habbo.plugin.events.furniture.FurniturePickedUpEvent; import com.eu.habbo.plugin.events.rooms.RoomLoadedEvent; @@ -75,6 +77,9 @@ public class Room implements Comparable, ISerialize, Runnable { private RoomRollerManager rollerManager; private RoomMessagingManager messagingManager; private RoomCycleManager cycleManager; + private RoomUserVariableManager userVariableManager; + private RoomFurniVariableManager furniVariableManager; + private RoomVariableManager roomVariableManager; public static final Comparator SORT_SCORE = (o1, o2) -> o2.getScore() - o1.getScore(); public static final Comparator SORT_ID = (o1, o2) -> o2.getId() - o1.getId(); @@ -92,6 +97,14 @@ public class Room implements Comparable, ISerialize, Runnable { public static int ROLLERS_MAXIMUM_ROLL_AVATARS = 1; public static boolean MUTEAREA_CAN_WHISPER = false; public static double MAXIMUM_FURNI_HEIGHT = 40d; + public static final int WIRED_ACCESS_EVERYONE = 1; + public static final int WIRED_ACCESS_USERS_WITH_RIGHTS = 2; + public static final int WIRED_ACCESS_GROUP_MEMBERS = 4; + public static final int WIRED_ACCESS_GROUP_ADMINS = 8; + public static final int WIRED_ACCESS_ALLOWED_INSPECT_MASK = WIRED_ACCESS_EVERYONE | WIRED_ACCESS_USERS_WITH_RIGHTS | WIRED_ACCESS_GROUP_MEMBERS | WIRED_ACCESS_GROUP_ADMINS; + public static final int WIRED_ACCESS_ALLOWED_MODIFY_MASK = WIRED_ACCESS_USERS_WITH_RIGHTS | WIRED_ACCESS_GROUP_MEMBERS | WIRED_ACCESS_GROUP_ADMINS; + public static final int WIRED_ACCESS_DEFAULT_INSPECT_MASK = 0; + public static final int WIRED_ACCESS_DEFAULT_MODIFY_MASK = 0; static { for (int i = 1; i <= 3; i++) { @@ -175,6 +188,10 @@ public class Room implements Comparable, ISerialize, Runnable { private volatile boolean muted; private RoomSpecialTypes roomSpecialTypes; private TraxManager traxManager; + private final Object wiredSettingsLock = new Object(); + private volatile boolean wiredSettingsLoaded; + private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK; + private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK; public final THashMap cache; @@ -269,6 +286,9 @@ public class Room implements Comparable, ISerialize, Runnable { this.rollerManager = new RoomRollerManager(this); this.messagingManager = new RoomMessagingManager(this); this.cycleManager = new RoomCycleManager(this); + this.userVariableManager = new RoomUserVariableManager(this); + this.furniVariableManager = new RoomFurniVariableManager(this); + this.roomVariableManager = new RoomVariableManager(this); } // ==================== MANAGER GETTERS ==================== @@ -350,6 +370,18 @@ public class Room implements Comparable, ISerialize, Runnable { return this.cycleManager; } + public RoomUserVariableManager getUserVariableManager() { + return this.userVariableManager; + } + + public RoomFurniVariableManager getFurniVariableManager() { + return this.furniVariableManager; + } + + public RoomVariableManager getRoomVariableManager() { + return this.roomVariableManager; + } + /** * Gets the roller manager for this room. */ @@ -924,13 +956,13 @@ public class Room implements Comparable, ISerialize, Runnable { this.itemManager.saveAllPendingItems(); + // Unregister all wired tickables for this room from the tick service + com.eu.habbo.habbohotel.wired.core.WiredManager.unregisterRoomTickables(this); + if (this.roomSpecialTypes != null) { this.roomSpecialTypes.dispose(); } - // Unregister all wired tickables for this room from the tick service - com.eu.habbo.habbohotel.wired.core.WiredManager.unregisterRoomTickables(this); - // Clear wired engine caches for this room if (com.eu.habbo.habbohotel.wired.core.WiredManager.getStackIndex() != null) { com.eu.habbo.habbohotel.wired.core.WiredManager.getStackIndex().invalidateAll(this); @@ -938,6 +970,8 @@ public class Room implements Comparable, ISerialize, Runnable { if (com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine() != null) { com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomRecursionDepth(this.id); com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomRateLimiters(this.id); + com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomBan(this.id); + com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomDiagnostics(this.id); } this.itemManager.clear(); @@ -2127,20 +2161,108 @@ public class Room implements Comparable, ISerialize, Runnable { return this.rightsManager.hasRights(habbo); } + public boolean hasExplicitRights(Habbo habbo) { + return habbo != null && this.rights.contains(habbo.getHabboInfo().getId()); + } + + public int getWiredInspectMask() { + this.ensureWiredSettingsLoaded(); + return this.wiredInspectMask; + } + + public int getWiredModifyMask() { + this.ensureWiredSettingsLoaded(); + return this.wiredModifyMask; + } + + public boolean canInspectWired(Habbo habbo) { + if (habbo == null) { + return false; + } + + if (this.canManageWiredSettings(habbo)) { + return true; + } + + this.ensureWiredSettingsLoaded(); + return this.matchesWiredAccessMask(habbo, this.wiredInspectMask, true); + } + + public boolean canModifyWired(Habbo habbo) { + if (habbo == null) { + return false; + } + + if (this.canManageWiredSettings(habbo)) { + return true; + } + + this.ensureWiredSettingsLoaded(); + return this.matchesWiredAccessMask(habbo, this.wiredModifyMask, false); + } + + public boolean canManageWiredSettings(Habbo habbo) { + return habbo != null && this.isOwner(habbo); + } + + public boolean saveWiredSettings(int inspectMask, int modifyMask) { + int sanitizedInspectMask = sanitizeWiredInspectMask(inspectMask); + int sanitizedModifyMask = sanitizeWiredModifyMask(modifyMask); + sanitizedInspectMask |= sanitizedModifyMask; + + synchronized (this.wiredSettingsLock) { + int previousInspectMask = this.wiredInspectMask; + int previousModifyMask = this.wiredModifyMask; + this.wiredInspectMask = sanitizedInspectMask; + this.wiredModifyMask = sanitizedModifyMask; + this.wiredSettingsLoaded = true; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) { + statement.setInt(1, this.id); + statement.setInt(2, sanitizedInspectMask); + statement.setInt(3, sanitizedModifyMask); + statement.executeUpdate(); + this.pushWiredSettingsToCurrentHabbos(); + return true; + } catch (SQLException e) { + this.wiredInspectMask = previousInspectMask; + this.wiredModifyMask = previousModifyMask; + LOGGER.error("Caught SQL exception while saving wired room settings", e); + return false; + } + } + } + public void giveRights(Habbo habbo) { - this.rightsManager.giveRights(habbo); + if (habbo == null) { + return; + } + + this.giveRights(habbo.getHabboInfo().getId()); } public void giveRights(int userId) { this.rightsManager.giveRights(userId); + + if (!this.rights.contains(userId)) { + this.rights.add(userId); + } + + this.pushWiredSettingsToCurrentHabbos(); } public void removeRights(int userId) { this.rightsManager.removeRights(userId); + this.rights.remove(userId); + this.pushWiredSettingsToCurrentHabbos(); } public void removeAllRights() { this.rightsManager.removeAllRights(); + this.rights.clear(); + this.pushWiredSettingsToCurrentHabbos(); } void refreshRightsInRoom() { @@ -2167,6 +2289,111 @@ public class Room implements Comparable, ISerialize, Runnable { return this.bannedHabbos; } + private void ensureWiredSettingsLoaded() { + if (this.wiredSettingsLoaded) { + return; + } + + synchronized (this.wiredSettingsLock) { + if (this.wiredSettingsLoaded) { + return; + } + + this.wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK; + this.wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT inspect_mask, modify_mask FROM room_wired_settings WHERE room_id = ? LIMIT 1")) { + statement.setInt(1, this.id); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + this.wiredInspectMask = sanitizeWiredInspectMask(set.getInt("inspect_mask")); + this.wiredModifyMask = sanitizeWiredModifyMask(set.getInt("modify_mask")); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while loading wired room settings", e); + } + + this.wiredSettingsLoaded = true; + } + } + + private boolean matchesWiredAccessMask(Habbo habbo, int mask, boolean allowEveryone) { + if (habbo == null) { + return false; + } + + if (allowEveryone && hasWiredAccess(mask, WIRED_ACCESS_EVERYONE)) { + return true; + } + + if (hasWiredAccess(mask, WIRED_ACCESS_USERS_WITH_RIGHTS) && this.hasExplicitRights(habbo)) { + return true; + } + + if (hasWiredAccess(mask, WIRED_ACCESS_GROUP_ADMINS) && this.isRoomGroupAdmin(habbo)) { + return true; + } + + return hasWiredAccess(mask, WIRED_ACCESS_GROUP_MEMBERS) && this.isRoomGroupMember(habbo); + } + + private boolean isRoomGroupMember(Habbo habbo) { + return habbo != null && this.guild > 0 && habbo.getHabboStats().hasGuild(this.guild); + } + + private boolean isRoomGroupAdmin(Habbo habbo) { + if (!this.isRoomGroupMember(habbo)) { + return false; + } + + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(this.guild, habbo.getHabboInfo().getId()); + + if (member == null) { + return false; + } + + GuildRank rank = member.getRank(); + return rank == GuildRank.OWNER || rank == GuildRank.ADMIN; + } + + private static boolean hasWiredAccess(int mask, int permissionMask) { + return (mask & permissionMask) != 0; + } + + private static int sanitizeWiredInspectMask(int mask) { + int sanitizedMask = mask & WIRED_ACCESS_ALLOWED_INSPECT_MASK; + + if (hasWiredAccess(sanitizedMask, WIRED_ACCESS_GROUP_MEMBERS)) { + sanitizedMask |= WIRED_ACCESS_GROUP_ADMINS; + } + + return sanitizedMask; + } + + private static int sanitizeWiredModifyMask(int mask) { + int sanitizedMask = mask & WIRED_ACCESS_ALLOWED_MODIFY_MASK; + + if (hasWiredAccess(sanitizedMask, WIRED_ACCESS_GROUP_MEMBERS)) { + sanitizedMask |= WIRED_ACCESS_GROUP_ADMINS; + } + + return sanitizedMask; + } + + private void pushWiredSettingsToCurrentHabbos() { + for (Habbo currentHabbo : this.getCurrentHabbos().values()) { + if (currentHabbo == null || currentHabbo.getClient() == null) { + continue; + } + + currentHabbo.getClient().sendResponse(new WiredRoomSettingsDataComposer(this, currentHabbo)); + } + } + public void addRoomBan(RoomBan roomBan) { this.rightsManager.addRoomBan(roomBan); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java index 6a2c537c..4b52804a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java @@ -328,7 +328,9 @@ public class RoomChatManager { suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput( habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), - wiredSayMessage); + wiredSayMessage, + chatType.ordinal(), + roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1); } } @@ -407,7 +409,12 @@ public class RoomChatManager { } if (chatType != RoomChatType.WHISPER && !ignoreWired && !roomChatMessage.isCommand) { - WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage); + WiredManager.triggerUserSays( + habbo.getHabboInfo().getCurrentRoom(), + habbo.getRoomUnit(), + wiredSayMessage, + chatType.ordinal(), + roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1); } // Notify bots and talking furniture diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java new file mode 100644 index 00000000..3f040ceb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java @@ -0,0 +1,1001 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class RoomFurniVariableManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RoomFurniVariableManager.class); + + private final Room room; + private final ConcurrentHashMap> activeAssignmentsByFurniId; + private volatile boolean permanentAssignmentsLoaded; + + public RoomFurniVariableManager(Room room) { + this.room = room; + this.activeAssignmentsByFurniId = new ConcurrentHashMap<>(); + this.permanentAssignmentsLoaded = false; + } + + public void ensurePermanentAssignmentsLoaded() { + if (this.permanentAssignmentsLoaded) { + return; + } + + synchronized (this) { + if (this.permanentAssignmentsLoaded) { + return; + } + + List staleRows = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT furni_id, variable_item_id, value, created_at, updated_at FROM room_furni_wired_variables WHERE room_id = ?")) { + statement.setInt(1, this.room.getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + int furniId = set.getInt("furni_id"); + int definitionItemId = set.getInt("variable_item_id"); + HabboItem furni = this.room.getHabboItem(furniId); + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + + if (furni == null || definition == null || !definition.isPermanentAvailability()) { + staleRows.add(new int[]{furniId, definitionItemId}); + continue; + } + + Integer value = null; + int rawValue = set.getInt("value"); + if (!set.wasNull()) { + value = rawValue; + } + + int createdAt = normalizeTimestamp(set.getInt("created_at"), 0); + int updatedAt = normalizeTimestamp(set.getInt("updated_at"), createdAt); + + this.activeAssignmentsByFurniId + .computeIfAbsent(furniId, key -> new ConcurrentHashMap<>()) + .put(definitionItemId, new VariableAssignment(value, createdAt, updatedAt)); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to restore wired furni variables for room {}", this.room.getId(), e); + } + + for (int[] staleRow : staleRows) { + this.deletePersistentAssignment(staleRow[0], staleRow[1]); + } + + this.permanentAssignmentsLoaded = true; + } + } + + public boolean assignVariable(HabboItem furni, WiredExtraFurniVariable definition, Integer value, boolean overrideExisting) { + return definition != null && this.assignVariable(furni, definition.getId(), value, overrideExisting); + } + + public boolean assignVariable(HabboItem furni, int definitionItemId, Integer value, boolean overrideExisting) { + if (furni == null || definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + boolean hadBefore = this.hasVariable(furni.getId(), definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(furni.getId(), definitionItemId) : null; + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).assignValue(this.room, furni.getId(), definitionInfo.hasValue() ? value : null, overrideExisting); + boolean shouldEmit = changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(furni.getId(), definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(furni.getId(), definitionItemId) : null; + this.emitVariableChangedEvents(furni.getId(), extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + if (definition == null) { + return false; + } + + this.ensurePermanentAssignmentsLoaded(); + + int furniId = furni.getId(); + Integer normalizedValue = definition.hasValue() ? value : null; + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.computeIfAbsent(furniId, key -> new ConcurrentHashMap<>()); + VariableAssignment existingAssignment = assignments.get(definitionItemId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + boolean changed = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + + if (existingAssignment == null) { + int now = Emulator.getIntUnixTimestamp(); + assignments.put(definitionItemId, new VariableAssignment(normalizedValue, now, now)); + } else if (changed) { + existingAssignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); + } + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(furniId, definitionItemId, assignments.get(definitionItemId)); + } else { + this.deletePersistentAssignment(furniId, definitionItemId); + } + + if (changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue))) { + boolean hasAfter = this.hasVariable(furniId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(furniId, definitionItemId) : null; + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + public boolean updateVariableValue(int furniId, int definitionItemId, Integer value) { + this.ensurePermanentAssignmentsLoaded(); + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || !definitionInfo.hasValue() || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(furniId, definitionItemId); + Integer previousValue = hadBefore ? this.getCurrentValue(furniId, definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).updateValue(this.room, furniId, value); + boolean shouldEmit = changed || (hadBefore && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(furniId, definitionItemId); + Integer currentValue = hasAfter ? this.getCurrentValue(furniId, definitionItemId) : null; + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + if (definition == null) { + return false; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return false; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null) { + return false; + } + + if (Objects.equals(assignment.getValue(), value)) { + this.emitVariableChangedEvents(furniId, extra, definitionInfo, true, previousValue, true, assignment.getValue()); + return false; + } + + assignment.setValue(value, Emulator.getIntUnixTimestamp()); + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(furniId, definitionItemId, assignment); + } + + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, true, assignment.getValue()); + this.broadcastSnapshot(); + return true; + } + + public int getCurrentValue(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + Integer baseValue = this.getRawValue(furniId, derivedDefinition.getBaseDefinitionItemId()); + Integer derivedValue = WiredVariableLevelSystemSupport.getDerivedValue(derivedDefinition.getLevelSystem(), derivedDefinition.getSubvariableType(), baseValue); + return (derivedValue != null) ? derivedValue : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCurrentValue(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + return 0; + } + + return assignment.getValue(); + } + + public int getCreatedAt(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(furniId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCreatedAt(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(furniId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getUpdatedAt(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + public boolean hasVariable(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + return this.getRawAssignment(furniId, derivedDefinition.getBaseDefinitionItemId()) != null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).hasVariable(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + return assignments != null && assignments.containsKey(definitionItemId); + } + + public boolean removeVariable(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(furniId, definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(furniId, definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).removeValue(this.room, furniId); + + if (changed) { + boolean hasAfter = this.hasVariable(furniId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(furniId, definitionItemId) : null; + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return false; + } + + if (assignments.remove(definitionItemId) == null) { + return false; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByFurniId.remove(furniId, assignments); + } + + this.deletePersistentAssignment(furniId, definitionItemId); + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, false, null); + this.broadcastSnapshot(); + + return true; + } + + public void removeAssignmentsForFurni(int furniId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0) { + return; + } + + boolean changed = (this.activeAssignmentsByFurniId.remove(furniId) != null); + this.deletePersistentAssignmentsForFurni(furniId); + + if (changed) { + this.broadcastSnapshot(); + } + } + + public void clearTransientAssignments() { + this.ensurePermanentAssignmentsLoaded(); + + boolean changed = false; + + for (Map.Entry> entry : this.activeAssignmentsByFurniId.entrySet()) { + ConcurrentHashMap assignments = entry.getValue(); + + for (Integer definitionItemId : new ArrayList<>(assignments.keySet())) { + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + + if (definition != null && definition.isPermanentAvailability()) { + continue; + } + + if (assignments.remove(definitionItemId) != null) { + changed = true; + } + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByFurniId.remove(entry.getKey(), assignments); + } + } + + if (changed) { + this.broadcastSnapshot(); + } + } + + public void removeDefinition(int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + boolean changed = false; + + for (Map.Entry> entry : this.activeAssignmentsByFurniId.entrySet()) { + ConcurrentHashMap assignments = entry.getValue(); + + if (assignments.remove(definitionItemId) != null) { + changed = true; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByFurniId.remove(entry.getKey(), assignments); + } + } + + this.deletePersistentAssignmentsForDefinition(definitionItemId); + this.broadcastSnapshot(); + } + + public void handleDefinitionUpdated(WiredExtraFurniVariable definition) { + if (definition == null) { + return; + } + + this.ensurePermanentAssignmentsLoaded(); + + if (!definition.isPermanentAvailability()) { + this.deletePersistentAssignmentsForDefinition(definition.getId()); + } else { + for (Map.Entry> entry : this.activeAssignmentsByFurniId.entrySet()) { + VariableAssignment assignment = entry.getValue().get(definition.getId()); + + if (assignment == null) continue; + + this.upsertPersistentAssignment(entry.getKey(), definition.getId(), assignment); + } + } + + this.broadcastSnapshot(); + } + + public Snapshot createSnapshot() { + this.ensurePermanentAssignmentsLoaded(); + + List definitions = new ArrayList<>(); + Map definitionsById = new LinkedHashMap<>(); + List derivedDefinitionIds = new ArrayList<>(); + List furniEchoes = this.getFurniEchoes(); + + for (WiredVariableDefinitionInfo definition : this.getAllDefinitionInfos()) { + DefinitionEntry entry = new DefinitionEntry(definition.getItemId(), definition.getName(), definition.hasValue(), definition.getAvailability(), definition.isTextConnected(), definition.isReadOnly()); + definitions.add(entry); + definitionsById.put(entry.getItemId(), entry); + + if (WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definition.getItemId()) != null) { + derivedDefinitionIds.add(definition.getItemId()); + } + } + + List furnis = new ArrayList<>(); + THashSet furniIds = new THashSet<>(); + furniIds.addAll(this.activeAssignmentsByFurniId.keySet()); + + for (HabboItem item : this.room.getFloorItems()) { + if (item != null) furniIds.add(item.getId()); + } + + for (HabboItem item : this.room.getWallItems()) { + if (item != null) furniIds.add(item.getId()); + } + + for (Integer furniId : furniIds) { + if (this.room.getHabboItem(furniId) == null) { + continue; + } + + List assignments = new ArrayList<>(); + ConcurrentHashMap localAssignments = this.activeAssignmentsByFurniId.get(furniId); + + if (localAssignments != null) { + for (Map.Entry assignmentEntry : localAssignments.entrySet()) { + if (!definitionsById.containsKey(assignmentEntry.getKey())) { + continue; + } + + assignments.add(new AssignmentEntry( + assignmentEntry.getKey(), + assignmentEntry.getValue().getValue(), + assignmentEntry.getValue().getCreatedAt(), + assignmentEntry.getValue().getUpdatedAt() + )); + } + } + + for (WiredExtraVariableEcho echo : furniEchoes) { + if (!definitionsById.containsKey(echo.getId()) || !echo.hasVariable(this.room, furniId)) { + continue; + } + + assignments.add(new AssignmentEntry( + echo.getId(), + echo.getCurrentValue(this.room, furniId), + echo.getCreatedAt(this.room, furniId), + echo.getUpdatedAt(this.room, furniId) + )); + } + + for (Integer derivedDefinitionId : derivedDefinitionIds) { + if (!this.hasVariable(furniId, derivedDefinitionId)) { + continue; + } + + assignments.add(new AssignmentEntry( + derivedDefinitionId, + this.getCurrentValue(furniId, derivedDefinitionId), + this.getCreatedAt(furniId, derivedDefinitionId), + this.getUpdatedAt(furniId, derivedDefinitionId) + )); + } + + assignments.sort(Comparator.comparingInt(AssignmentEntry::getVariableItemId)); + + if (!assignments.isEmpty()) { + furnis.add(new FurniAssignmentsEntry(furniId, assignments)); + } + } + + furnis.sort(Comparator.comparingInt(FurniAssignmentsEntry::getFurniId)); + + return new Snapshot(this.room.getId(), definitions, furnis); + } + + public void sendSnapshot(Habbo habbo) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + return; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.createSnapshot(), this.room.getRoomVariableManager().createSnapshot())); + } + + public void broadcastSnapshot() { + RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot(); + Snapshot furniSnapshot = this.createSnapshot(); + RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot(); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + continue; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(userSnapshot, furniSnapshot, roomSnapshot)); + } + } + + public Collection getDefinitions() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet extras = this.room.getRoomSpecialTypes().getExtras(); + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraFurniVariable) { + result.add((WiredExtraFurniVariable) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraFurniVariable::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraFurniVariable::getId)); + return result; + } + + public Collection getAllDefinitionInfos() { + List result = new ArrayList<>(); + List baseDefinitions = new ArrayList<>(); + + for (WiredExtraFurniVariable definition : this.getDefinitions()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + )); + } + + for (WiredExtraVariableEcho echo : this.getFurniEchoes()) { + baseDefinitions.add(echo.createDefinitionInfo(this.room)); + } + + result.addAll(baseDefinitions); + + for (WiredVariableDefinitionInfo definition : baseDefinitions) { + result.addAll(WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, this.getDefinitionExtra(definition.getItemId()), definition)); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public boolean hasDefinition(int definitionItemId) { + return this.getDefinitionInfo(definitionItemId) != null; + } + + public WiredVariableDefinitionInfo getDefinitionInfo(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (extra instanceof WiredExtraFurniVariable) { + WiredExtraFurniVariable definition = (WiredExtraFurniVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isFurniEcho()) { + return ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + } + + return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + } + + private WiredExtraFurniVariable getDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (!(extra instanceof WiredExtraFurniVariable)) { + return null; + } + + return (WiredExtraFurniVariable) extra; + } + + private InteractionWiredExtra getDefinitionExtra(int definitionItemId) { + if (this.room.getRoomSpecialTypes() == null) { + return null; + } + + return this.room.getRoomSpecialTypes().getExtra(definitionItemId); + } + + private List getFurniEchoes() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isFurniEcho()) { + result.add((WiredExtraVariableEcho) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableEcho::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableEcho::getId)); + return result; + } + + private VariableAssignment getRawAssignment(int furniId, int definitionItemId) { + if (furniId <= 0 || definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + if (!echo.hasVariable(this.room, furniId)) { + return null; + } + + return new VariableAssignment(echo.getCurrentValue(this.room, furniId), echo.getCreatedAt(this.room, furniId), echo.getUpdatedAt(this.room, furniId)); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + return (assignments != null) ? assignments.get(definitionItemId) : null; + } + + private Integer getRawValue(int furniId, int definitionItemId) { + VariableAssignment assignment = this.getRawAssignment(furniId, definitionItemId); + return (assignment != null) ? assignment.getValue() : null; + } + + private void emitVariableChangedEvents(int furniId, InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo definitionInfo, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (definitionInfo == null) { + return; + } + + this.emitVariableChangedEvent(furniId, definitionInfo.getItemId(), definitionInfo.hasValue(), existedBefore, previousValue, existsAfter, currentValue); + + for (WiredVariableDefinitionInfo derivedDefinition : WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionExtra, definitionInfo)) { + WiredVariableLevelSystemSupport.DerivedDefinition resolvedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, derivedDefinition.getItemId()); + + if (resolvedDefinition == null) { + continue; + } + + Integer derivedPreviousValue = existedBefore + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), previousValue) + : null; + Integer derivedCurrentValue = existsAfter + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), currentValue) + : null; + + this.emitVariableChangedEvent(furniId, derivedDefinition.getItemId(), true, existedBefore, derivedPreviousValue, existsAfter, derivedCurrentValue); + } + } + + private void emitVariableChangedEvent(int furniId, int definitionItemId, boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + boolean created = !existedBefore && existsAfter; + boolean deleted = existedBefore && !existsAfter; + WiredEvent.VariableChangeKind changeKind = resolveVariableChangeKind(hasValue, existedBefore, previousValue, existsAfter, currentValue); + + if (!created && !deleted && changeKind == WiredEvent.VariableChangeKind.NONE) { + return; + } + + WiredManager.triggerFurniVariableChanged(this.room, furniId, definitionItemId, created, deleted, changeKind); + } + + private static WiredEvent.VariableChangeKind resolveVariableChangeKind(boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (!hasValue || !existedBefore || !existsAfter) { + return WiredEvent.VariableChangeKind.NONE; + } + + if (Objects.equals(previousValue, currentValue)) { + return WiredEvent.VariableChangeKind.UNCHANGED; + } + + int previousNumericValue = (previousValue != null) ? previousValue : 0; + int currentNumericValue = (currentValue != null) ? currentValue : 0; + + return (currentNumericValue > previousNumericValue) + ? WiredEvent.VariableChangeKind.INCREASED + : WiredEvent.VariableChangeKind.DECREASED; + } + + private void upsertPersistentAssignment(int furniId, int definitionItemId, VariableAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_furni_wired_variables (room_id, furni_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, furniId); + statement.setInt(3, definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + statement.setNull(4, java.sql.Types.INTEGER); + } else { + statement.setInt(4, assignment.getValue()); + } + + int now = Emulator.getIntUnixTimestamp(); + statement.setInt(5, (assignment != null) ? assignment.getCreatedAt() : now); + statement.setInt(6, (assignment != null) ? assignment.getUpdatedAt() : now); + + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store permanent wired furni variable for room {}, furni {}, item {}", this.room.getId(), furniId, definitionItemId, e); + } + } + + private void deletePersistentAssignment(int furniId, int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_furni_wired_variables WHERE room_id = ? AND furni_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, furniId); + statement.setInt(3, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired furni variable for room {}, furni {}, item {}", this.room.getId(), furniId, definitionItemId, e); + } + } + + private void deletePersistentAssignmentsForDefinition(int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_furni_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired furni variables for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + private void deletePersistentAssignmentsForFurni(int furniId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_furni_wired_variables WHERE room_id = ? AND furni_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, furniId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired furni variables for room {} and furni {}", this.room.getId(), furniId, e); + } + } + + public static class Snapshot { + private final int roomId; + private final List definitions; + private final List furnis; + + public Snapshot(int roomId, List definitions, List furnis) { + this.roomId = roomId; + this.definitions = definitions; + this.furnis = furnis; + } + + public int getRoomId() { + return this.roomId; + } + + public List getDefinitions() { + return this.definitions; + } + + public List getFurnis() { + return this.furnis; + } + } + + public static class DefinitionEntry { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public DefinitionEntry(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } + + public static class FurniAssignmentsEntry { + private final int furniId; + private final List assignments; + + public FurniAssignmentsEntry(int furniId, List assignments) { + this.furniId = furniId; + this.assignments = assignments; + } + + public int getFurniId() { + return this.furniId; + } + + public List getAssignments() { + return this.assignments; + } + } + + public static class AssignmentEntry { + private final int variableItemId; + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public AssignmentEntry(int variableItemId, Integer value, int createdAt, int updatedAt) { + this.variableItemId = variableItemId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public boolean hasValue() { + return this.value != null; + } + + public Integer getValue() { + return this.value; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static class VariableAssignment { + private Integer value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return this.value; + } + + public void setValue(Integer value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) return value; + if (fallback > 0) return fallback; + return Emulator.getIntUnixTimestamp(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index d3c11dab..a52b9c3e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -16,6 +16,13 @@ import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagPole; import com.eu.habbo.habbohotel.items.interactions.pets.*; import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; @@ -773,6 +780,8 @@ public class RoomItemManager { if (specialTypes == null) { return; } + + this.room.getFurniVariableManager().removeAssignmentsForFurni(item.getId()); boolean isWiredItem = false; @@ -785,21 +794,50 @@ public class RoomItemManager { specialTypes.removeCycleTask((ICycleable) item); } - if (item instanceof InteractionBattleBanzaiTeleporter) { - specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item); - } else if (item instanceof InteractionWiredTrigger) { - specialTypes.removeTrigger((InteractionWiredTrigger) item); - isWiredItem = true; - } else if (item instanceof InteractionWiredEffect) { - specialTypes.removeEffect((InteractionWiredEffect) item); - isWiredItem = true; - } else if (item instanceof InteractionWiredCondition) { - specialTypes.removeCondition((InteractionWiredCondition) item); - isWiredItem = true; - } else if (item instanceof InteractionWiredExtra) { - specialTypes.removeExtra((InteractionWiredExtra) item); - isWiredItem = true; - } else if (item instanceof InteractionRoller) { + if (item instanceof InteractionBattleBanzaiTeleporter) { + specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item); + } else if (item instanceof InteractionWiredTrigger) { + specialTypes.removeTrigger((InteractionWiredTrigger) item); + isWiredItem = true; + } else if (item instanceof InteractionWiredEffect) { + specialTypes.removeEffect((InteractionWiredEffect) item); + isWiredItem = true; + } else if (item instanceof InteractionWiredCondition) { + specialTypes.removeCondition((InteractionWiredCondition) item); + isWiredItem = true; + } else if (item instanceof InteractionWiredExtra) { + boolean removedContextDefinition = false; + if (item instanceof WiredExtraUserVariable) { + this.room.getUserVariableManager().removeDefinition(item.getId()); + } else if (item instanceof WiredExtraFurniVariable) { + this.room.getFurniVariableManager().removeDefinition(item.getId()); + } else if (item instanceof WiredExtraRoomVariable) { + this.room.getRoomVariableManager().removeDefinition(item.getId()); + } else if (item instanceof WiredExtraContextVariable) { + removedContextDefinition = true; + } else if (item instanceof WiredExtraVariableReference) { + if (((WiredExtraVariableReference) item).isRoomReference()) { + this.room.getRoomVariableManager().removeDefinition(item.getId()); + } else { + this.room.getUserVariableManager().removeDefinition(item.getId()); + } + } else if (item instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item; + + if (echo.isRoomEcho()) { + this.room.getRoomVariableManager().removeDefinition(item.getId()); + } else if (echo.isFurniEcho()) { + this.room.getFurniVariableManager().removeDefinition(item.getId()); + } else { + this.room.getUserVariableManager().removeDefinition(item.getId()); + } + } + specialTypes.removeExtra((InteractionWiredExtra) item); + if (removedContextDefinition) { + WiredContextVariableSupport.broadcastDefinitions(this.room); + } + isWiredItem = true; + } else if (item instanceof InteractionRoller) { specialTypes.removeRoller((InteractionRoller) item); } else if (item instanceof InteractionGameScoreboard) { specialTypes.removeScoreboard((InteractionGameScoreboard) item); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java index fff73a0b..08d9ccca 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java @@ -780,6 +780,7 @@ public class RoomManager { habbo.getRoomUnit().setInvisible(false); room.addHabbo(habbo); + room.getUserVariableManager().restorePermanentAssignments(habbo); // Pre-send own wearing badges so the client cache is populated before the user clicks themselves habbo.getClient().sendResponse(new UserBadgesComposer(habbo.getInventory().getBadgesComponent().getWearingBadges(), habbo.getHabboInfo().getId())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java index e18b3823..ce3537c5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java @@ -232,12 +232,14 @@ public class RoomUnitManager { habbo.getRoomUnit().getCurrentLocation().removeUnit(habbo.getRoomUnit()); } - synchronized (this.room.roomUnitLock) { - this.currentHabbos.remove(habbo.getHabboInfo().getId()); - } + synchronized (this.room.roomUnitLock) { + this.currentHabbos.remove(habbo.getHabboInfo().getId()); + } - if (sendRemovePacket && habbo.getRoomUnit() != null && !habbo.getRoomUnit().isTeleporting) { - this.room.sendComposer(new RoomUserRemoveComposer(habbo.getRoomUnit()).compose()); + this.room.getUserVariableManager().clearAssignmentsForUser(habbo.getHabboInfo().getId()); + + if (sendRemovePacket && habbo.getRoomUnit() != null && !habbo.getRoomUnit().isTeleporting) { + this.room.sendComposer(new RoomUserRemoveComposer(habbo.getRoomUnit()).compose()); } if (habbo.getRoomUnit().getCurrentLocation() != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java new file mode 100644 index 00000000..a05df8c6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java @@ -0,0 +1,1101 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredVariableReferenceSupport; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class RoomUserVariableManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserVariableManager.class); + + private final Room room; + private final ConcurrentHashMap> activeAssignmentsByUserId; + + public RoomUserVariableManager(Room room) { + this.room = room; + this.activeAssignmentsByUserId = new ConcurrentHashMap<>(); + } + + public void restorePermanentAssignments(Habbo habbo) { + if (habbo == null) { + return; + } + + int userId = habbo.getHabboInfo().getId(); + ConcurrentHashMap restoredAssignments = new ConcurrentHashMap<>(); + List staleDefinitionIds = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT variable_item_id, value, created_at, updated_at FROM room_user_wired_variables WHERE room_id = ? AND user_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, userId); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + int definitionItemId = set.getInt("variable_item_id"); + WiredExtraUserVariable definition = this.getDefinition(definitionItemId); + + if (definition == null || !definition.isPermanentAvailability()) { + staleDefinitionIds.add(definitionItemId); + continue; + } + + Integer value = null; + int rawValue = set.getInt("value"); + if (!set.wasNull()) { + value = rawValue; + } + + int createdAt = normalizeTimestamp(set.getInt("created_at"), 0); + int updatedAt = normalizeTimestamp(set.getInt("updated_at"), createdAt); + + restoredAssignments.put(definitionItemId, new VariableAssignment(value, createdAt, updatedAt)); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to restore wired user variables for room {} and user {}", this.room.getId(), userId, e); + } + + if (!staleDefinitionIds.isEmpty()) { + for (Integer definitionItemId : staleDefinitionIds) { + this.deletePersistentAssignment(userId, definitionItemId); + } + } + + if (restoredAssignments.isEmpty()) { + this.activeAssignmentsByUserId.remove(userId); + } else { + this.activeAssignmentsByUserId.put(userId, restoredAssignments); + } + + this.broadcastSnapshot(); + } + + public boolean assignVariable(Habbo habbo, WiredExtraUserVariable definition, Integer value, boolean overrideExisting) { + return definition != null && this.assignVariable(habbo, definition.getId(), value, overrideExisting); + } + + public boolean assignVariable(Habbo habbo, int definitionItemId, Integer value, boolean overrideExisting) { + if (habbo == null || definitionItemId <= 0) { + return false; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + int userId = habbo.getHabboInfo().getId(); + Integer normalizedValue = definitionInfo.hasValue() ? value : null; + boolean hadBefore = this.hasVariable(userId, definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(userId, definitionItemId) : null; + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.assignSharedUserVariable((WiredExtraVariableReference) extra, userId, normalizedValue, overrideExisting); + boolean shouldEmit = changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).assignValue(this.room, userId, normalizedValue, overrideExisting); + boolean shouldEmit = changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.computeIfAbsent(userId, key -> new ConcurrentHashMap<>()); + VariableAssignment existingAssignment = assignments.get(definitionItemId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + boolean changed = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + + if (existingAssignment == null) { + int now = Emulator.getIntUnixTimestamp(); + assignments.put(definitionItemId, new VariableAssignment(normalizedValue, now, now)); + } else if (changed) { + existingAssignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); + } + + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(userId, definitionItemId, assignments.get(definitionItemId)); + } else { + this.deletePersistentAssignment(userId, definitionItemId); + } + + if (changed) { + if (definition.isSharedAvailability()) { + VariableAssignment assignment = assignments.get(definitionItemId); + if (assignment != null) { + WiredVariableReferenceSupport.cacheSharedUserAssignment(this.room.getId(), definitionItemId, userId, assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()); + } + } else { + WiredVariableReferenceSupport.clearSharedUserAssignment(this.room.getId(), definitionItemId, userId); + } + } + + if (changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue))) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + public boolean updateVariableValue(int userId, int definitionItemId, Integer value) { + if (userId <= 0 || definitionItemId <= 0) { + return false; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + + if (definitionInfo == null || !definitionInfo.hasValue() || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(userId, definitionItemId); + Integer previousValue = hadBefore ? this.getCurrentValue(userId, definitionItemId) : null; + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.updateSharedUserVariable((WiredExtraVariableReference) extra, userId, value); + boolean shouldEmit = changed || (hadBefore && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = hasAfter ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).updateValue(this.room, userId, value); + boolean shouldEmit = changed || (hadBefore && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = hasAfter ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return false; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null) { + return false; + } + + Integer normalizedValue = value; + if (Objects.equals(assignment.getValue(), normalizedValue)) { + this.emitVariableChangedEvents(userId, extra, definitionInfo, true, previousValue, true, assignment.getValue()); + return false; + } + + assignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); + + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(userId, definitionItemId, assignment); + } + + if (definition.isSharedAvailability()) { + WiredVariableReferenceSupport.cacheSharedUserAssignment(this.room.getId(), definitionItemId, userId, assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()); + } + + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, true, assignment.getValue()); + this.broadcastSnapshot(); + return true; + } + + public int getCurrentValue(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + Integer baseValue = this.getRawValue(userId, derivedDefinition.getBaseDefinitionItemId()); + Integer derivedValue = WiredVariableLevelSystemSupport.getDerivedValue(derivedDefinition.getLevelSystem(), derivedDefinition.getSubvariableType(), baseValue); + return (derivedValue != null) ? derivedValue : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return (assignment != null && assignment.getValue() != null) ? assignment.getValue() : 0; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCurrentValue(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + return 0; + } + + return assignment.getValue(); + } + + public int getCreatedAt(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(userId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCreatedAt(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(userId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getUpdatedAt(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + public boolean hasVariable(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + return this.getRawAssignment(userId, derivedDefinition.getBaseDefinitionItemId()) != null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + return WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId) != null; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).hasVariable(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + return assignments != null && assignments.containsKey(definitionItemId); + } + + public boolean removeVariable(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(userId, definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(userId, definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.removeSharedUserVariable((WiredExtraVariableReference) extra, userId); + + if (changed) { + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, false, null); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).removeValue(this.room, userId); + + if (changed) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return false; + } + + if (assignments.remove(definitionItemId) == null) { + return false; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByUserId.remove(userId, assignments); + } + + this.deletePersistentAssignment(userId, definitionItemId); + + WiredExtraUserVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedUserAssignment(this.room.getId(), definitionItemId, userId); + } + + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, false, null); + this.broadcastSnapshot(); + + return true; + } + + public void clearAssignmentsForUser(int userId) { + if (userId <= 0) { + return; + } + + if (this.activeAssignmentsByUserId.remove(userId) != null) { + this.broadcastSnapshot(); + } + } + + public void removeDefinition(int definitionItemId) { + boolean changed = false; + + for (Map.Entry> entry : this.activeAssignmentsByUserId.entrySet()) { + ConcurrentHashMap assignments = entry.getValue(); + if (assignments.remove(definitionItemId) != null) { + changed = true; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByUserId.remove(entry.getKey(), assignments); + } + } + + this.deletePersistentAssignmentsForDefinition(definitionItemId); + WiredExtraUserVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedUserDefinition(this.room.getId(), definitionItemId); + } + + if (changed) { + this.broadcastSnapshot(); + return; + } + + this.broadcastSnapshot(); + } + + public void handleDefinitionUpdated(WiredExtraUserVariable definition) { + if (definition == null) { + return; + } + + if (!definition.isPermanentAvailability()) { + this.deletePersistentAssignmentsForDefinition(definition.getId()); + } else { + for (Map.Entry> entry : this.activeAssignmentsByUserId.entrySet()) { + VariableAssignment assignment = entry.getValue().get(definition.getId()); + + if (assignment == null) continue; + + this.upsertPersistentAssignment(entry.getKey(), definition.getId(), assignment); + } + } + + if (definition.isSharedAvailability()) { + for (Map.Entry> entry : this.activeAssignmentsByUserId.entrySet()) { + VariableAssignment assignment = entry.getValue().get(definition.getId()); + + if (assignment == null) { + continue; + } + + WiredVariableReferenceSupport.cacheSharedUserAssignment(this.room.getId(), definition.getId(), entry.getKey(), assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()); + } + } else { + WiredVariableReferenceSupport.clearSharedUserDefinition(this.room.getId(), definition.getId()); + } + + this.broadcastSnapshot(); + } + + public Snapshot createSnapshot() { + List definitions = new ArrayList<>(); + Map definitionsById = new LinkedHashMap<>(); + List derivedDefinitionIds = new ArrayList<>(); + + for (WiredVariableDefinitionInfo definition : this.getAllDefinitionInfos()) { + DefinitionEntry entry = new DefinitionEntry(definition.getItemId(), definition.getName(), definition.hasValue(), definition.getAvailability(), definition.isTextConnected(), definition.isReadOnly()); + definitions.add(entry); + definitionsById.put(entry.getItemId(), entry); + + if (WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definition.getItemId()) != null) { + derivedDefinitionIds.add(definition.getItemId()); + } + } + + List users = new ArrayList<>(); + List userReferences = this.getUserReferences(); + List userEchoes = this.getUserEchoes(); + THashSet userIds = new THashSet<>(); + userIds.addAll(this.activeAssignmentsByUserId.keySet()); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo != null) { + userIds.add(habbo.getHabboInfo().getId()); + } + } + + for (Integer userId : userIds) { + List assignments = new ArrayList<>(); + ConcurrentHashMap localAssignments = this.activeAssignmentsByUserId.get(userId); + + if (localAssignments != null) { + for (Map.Entry assignmentEntry : localAssignments.entrySet()) { + if (!definitionsById.containsKey(assignmentEntry.getKey())) { + continue; + } + + assignments.add(new AssignmentEntry( + assignmentEntry.getKey(), + assignmentEntry.getValue().getValue(), + assignmentEntry.getValue().getCreatedAt(), + assignmentEntry.getValue().getUpdatedAt() + )); + } + } + + for (WiredExtraVariableReference reference : userReferences) { + if (!definitionsById.containsKey(reference.getId())) { + continue; + } + + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment(reference, userId); + if (assignment == null) { + continue; + } + + assignments.add(new AssignmentEntry(reference.getId(), assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt())); + } + + for (WiredExtraVariableEcho echo : userEchoes) { + if (!definitionsById.containsKey(echo.getId()) || !echo.hasVariable(this.room, userId)) { + continue; + } + + assignments.add(new AssignmentEntry( + echo.getId(), + echo.getCurrentValue(this.room, userId), + echo.getCreatedAt(this.room, userId), + echo.getUpdatedAt(this.room, userId) + )); + } + + for (Integer derivedDefinitionId : derivedDefinitionIds) { + if (!this.hasVariable(userId, derivedDefinitionId)) { + continue; + } + + assignments.add(new AssignmentEntry( + derivedDefinitionId, + this.getCurrentValue(userId, derivedDefinitionId), + this.getCreatedAt(userId, derivedDefinitionId), + this.getUpdatedAt(userId, derivedDefinitionId) + )); + } + + assignments.sort(Comparator.comparingInt(AssignmentEntry::getVariableItemId)); + + if (!assignments.isEmpty()) { + users.add(new UserAssignmentsEntry(userId, assignments)); + } + } + + users.sort(Comparator.comparingInt(UserAssignmentsEntry::getUserId)); + + return new Snapshot(this.room.getId(), definitions, users); + } + + public void sendSnapshot(Habbo habbo) { + if (habbo == null || habbo.getClient() == null) { + return; + } + + if (!this.room.canInspectWired(habbo)) { + return; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.room.getRoomVariableManager().createSnapshot())); + } + + public void broadcastSnapshot() { + Snapshot userSnapshot = this.createSnapshot(); + RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot(); + RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot(); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo == null || habbo.getClient() == null) { + continue; + } + + if (!this.room.canInspectWired(habbo)) { + continue; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(userSnapshot, furniSnapshot, roomSnapshot)); + } + } + + public Collection getDefinitions() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet extras = this.room.getRoomSpecialTypes().getExtras(); + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraUserVariable) { + result.add((WiredExtraUserVariable) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraUserVariable::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraUserVariable::getId)); + return result; + } + + public Collection getAllDefinitionInfos() { + List result = new ArrayList<>(); + List baseDefinitions = new ArrayList<>(); + + for (WiredExtraUserVariable definition : this.getDefinitions()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + )); + } + + for (WiredExtraVariableReference reference : this.getUserReferences()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + reference.getId(), + reference.getVariableName(), + reference.hasValue(), + reference.getAvailability(), + false, + reference.isReadOnly() + )); + } + + for (WiredExtraVariableEcho echo : this.getUserEchoes()) { + baseDefinitions.add(echo.createDefinitionInfo(this.room)); + } + + result.addAll(baseDefinitions); + + for (WiredVariableDefinitionInfo definition : baseDefinitions) { + result.addAll(WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_USER, this.getDefinitionExtra(definition.getItemId()), definition)); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public boolean hasDefinition(int definitionItemId) { + return this.getDefinitionInfo(definitionItemId) != null; + } + + public WiredVariableDefinitionInfo getDefinitionInfo(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (extra instanceof WiredExtraUserVariable) { + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isUserEcho()) { + return ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + } + + return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + } + + private WiredExtraUserVariable getDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (!(extra instanceof WiredExtraUserVariable)) { + return null; + } + + return (WiredExtraUserVariable) extra; + } + + private InteractionWiredExtra getDefinitionExtra(int definitionItemId) { + if (this.room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return null; + } + + return this.room.getRoomSpecialTypes().getExtra(definitionItemId); + } + + private List getUserReferences() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { + result.add((WiredExtraVariableReference) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableReference::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableReference::getId)); + return result; + } + + private List getUserEchoes() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isUserEcho()) { + result.add((WiredExtraVariableEcho) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableEcho::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableEcho::getId)); + return result; + } + + private VariableAssignment getRawAssignment(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return (assignment != null) ? new VariableAssignment(assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()) : null; + } + + if (extra instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + if (!echo.hasVariable(this.room, userId)) { + return null; + } + + return new VariableAssignment(echo.getCurrentValue(this.room, userId), echo.getCreatedAt(this.room, userId), echo.getUpdatedAt(this.room, userId)); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + return (assignments != null) ? assignments.get(definitionItemId) : null; + } + + private Integer getRawValue(int userId, int definitionItemId) { + VariableAssignment assignment = this.getRawAssignment(userId, definitionItemId); + return (assignment != null) ? assignment.getValue() : null; + } + + private void emitVariableChangedEvents(int userId, InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo definitionInfo, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (definitionInfo == null) { + return; + } + + this.emitVariableChangedEvent(userId, definitionInfo.getItemId(), definitionInfo.hasValue(), existedBefore, previousValue, existsAfter, currentValue); + + for (WiredVariableDefinitionInfo derivedDefinition : WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionExtra, definitionInfo)) { + WiredVariableLevelSystemSupport.DerivedDefinition resolvedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, derivedDefinition.getItemId()); + + if (resolvedDefinition == null) { + continue; + } + + Integer derivedPreviousValue = existedBefore + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), previousValue) + : null; + Integer derivedCurrentValue = existsAfter + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), currentValue) + : null; + + this.emitVariableChangedEvent(userId, derivedDefinition.getItemId(), true, existedBefore, derivedPreviousValue, existsAfter, derivedCurrentValue); + } + } + + private void emitVariableChangedEvent(int userId, int definitionItemId, boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + boolean created = !existedBefore && existsAfter; + boolean deleted = existedBefore && !existsAfter; + WiredEvent.VariableChangeKind changeKind = resolveVariableChangeKind(hasValue, existedBefore, previousValue, existsAfter, currentValue); + + if (!created && !deleted && changeKind == WiredEvent.VariableChangeKind.NONE) { + return; + } + + WiredManager.triggerUserVariableChanged(this.room, userId, definitionItemId, created, deleted, changeKind); + } + + private static WiredEvent.VariableChangeKind resolveVariableChangeKind(boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (!hasValue || !existedBefore || !existsAfter) { + return WiredEvent.VariableChangeKind.NONE; + } + + if (Objects.equals(previousValue, currentValue)) { + return WiredEvent.VariableChangeKind.UNCHANGED; + } + + int previousNumericValue = (previousValue != null) ? previousValue : 0; + int currentNumericValue = (currentValue != null) ? currentValue : 0; + + return (currentNumericValue > previousNumericValue) + ? WiredEvent.VariableChangeKind.INCREASED + : WiredEvent.VariableChangeKind.DECREASED; + } + + private void upsertPersistentAssignment(int userId, int definitionItemId, VariableAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, userId); + statement.setInt(3, definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + statement.setNull(4, java.sql.Types.INTEGER); + } else { + statement.setInt(4, assignment.getValue()); + } + + int now = Emulator.getIntUnixTimestamp(); + statement.setInt(5, (assignment != null) ? assignment.getCreatedAt() : now); + statement.setInt(6, (assignment != null) ? assignment.getUpdatedAt() : now); + + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store permanent wired user variable for room {}, user {}, item {}", this.room.getId(), userId, definitionItemId, e); + } + } + + private void deletePersistentAssignment(int userId, int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, userId); + statement.setInt(3, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired user variable for room {}, user {}, item {}", this.room.getId(), userId, definitionItemId, e); + } + } + + private void deletePersistentAssignmentsForDefinition(int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired user variables for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + public static class Snapshot { + private final int roomId; + private final List definitions; + private final List users; + + public Snapshot(int roomId, List definitions, List users) { + this.roomId = roomId; + this.definitions = definitions; + this.users = users; + } + + public int getRoomId() { + return roomId; + } + + public List getDefinitions() { + return definitions; + } + + public List getUsers() { + return users; + } + } + + public static class DefinitionEntry { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public DefinitionEntry(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return itemId; + } + + public String getName() { + return name; + } + + public boolean hasValue() { + return hasValue; + } + + public int getAvailability() { + return availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } + + public static class UserAssignmentsEntry { + private final int userId; + private final List assignments; + + public UserAssignmentsEntry(int userId, List assignments) { + this.userId = userId; + this.assignments = assignments; + } + + public int getUserId() { + return userId; + } + + public List getAssignments() { + return assignments; + } + } + + public static class AssignmentEntry { + private final int variableItemId; + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public AssignmentEntry(int variableItemId, Integer value, int createdAt, int updatedAt) { + this.variableItemId = variableItemId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getVariableItemId() { + return variableItemId; + } + + public boolean hasValue() { + return value != null; + } + + public Integer getValue() { + return value; + } + + public int getCreatedAt() { + return createdAt; + } + + public int getUpdatedAt() { + return updatedAt; + } + } + + private static class VariableAssignment { + private Integer value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getCreatedAt() { + return createdAt; + } + + public int getUpdatedAt() { + return updatedAt; + } + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) return value; + if (fallback > 0) return fallback; + return Emulator.getIntUnixTimestamp(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java new file mode 100644 index 00000000..25b87649 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java @@ -0,0 +1,827 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredVariableReferenceSupport; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class RoomVariableManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RoomVariableManager.class); + + private final Room room; + private final ConcurrentHashMap activeAssignmentsByDefinitionId; + private volatile boolean persistentValuesLoaded; + + public RoomVariableManager(Room room) { + this.room = room; + this.activeAssignmentsByDefinitionId = new ConcurrentHashMap<>(); + this.persistentValuesLoaded = false; + } + + public void ensurePersistentValuesLoaded() { + if (this.persistentValuesLoaded) { + return; + } + + synchronized (this) { + if (this.persistentValuesLoaded) { + return; + } + + List staleDefinitionIds = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT variable_item_id, value, created_at, updated_at FROM room_wired_variables WHERE room_id = ?")) { + statement.setInt(1, this.room.getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + int definitionItemId = set.getInt("variable_item_id"); + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + + if (definition == null || !definition.isPermanentAvailability()) { + staleDefinitionIds.add(definitionItemId); + continue; + } + + int updatedAt = normalizeTimestamp(set.getInt("updated_at"), 0); + + this.activeAssignmentsByDefinitionId.put(definitionItemId, new VariableAssignment(set.getInt("value"), 0, updatedAt)); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to restore wired room variables for room {}", this.room.getId(), e); + } + + for (Integer definitionItemId : staleDefinitionIds) { + this.deletePersistentAssignment(definitionItemId); + } + + this.persistentValuesLoaded = true; + } + } + + public int getCurrentValue(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + Integer baseValue = this.getRawValue(derivedDefinition.getBaseDefinitionItemId()); + Integer derivedValue = WiredVariableLevelSystemSupport.getDerivedValue(derivedDefinition.getLevelSystem(), derivedDefinition.getSubvariableType(), baseValue); + return (derivedValue != null) ? derivedValue : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCurrentValue(this.room, this.room.getId()); + } + + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment((WiredExtraVariableReference) extra); + return assignment != null ? assignment.getValue() : 0; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + + return (assignment != null) ? assignment.getValue() : 0; + } + + public int getCreatedAt(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCreatedAt(this.room, this.room.getId()); + } + + if (extra instanceof WiredExtraVariableReference) { + return 0; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getUpdatedAt(this.room, this.room.getId()); + } + + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment((WiredExtraVariableReference) extra); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + public boolean hasVariable(int definitionItemId) { + if (definitionItemId <= 0) { + return false; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + return this.getRawAssignment(derivedDefinition.getBaseDefinitionItemId()) != null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).hasVariable(this.room, this.room.getId()); + } + + return this.getDefinitionInfo(definitionItemId) != null; + } + + public boolean updateVariableValue(int definitionItemId, int value) { + this.ensurePersistentValuesLoaded(); + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + Integer previousValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).updateValue(this.room, this.room.getId(), value); + boolean shouldEmit = changed || (definitionInfo.hasValue() && previousValue != null && previousValue == value); + + if (shouldEmit) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.updateSharedRoomVariable((WiredExtraVariableReference) extra, value); + boolean shouldEmit = changed || (definitionInfo.hasValue() && previousValue != null && previousValue == value); + + if (shouldEmit) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + + if (assignment == null) { + assignment = new VariableAssignment(value, 0, Emulator.getIntUnixTimestamp()); + this.activeAssignmentsByDefinitionId.put(definitionItemId, assignment); + } else if (assignment.getValue() == value) { + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, assignment.getValue()); + return false; + } else { + assignment.setValue(value, Emulator.getIntUnixTimestamp()); + } + + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(definitionItemId, assignment); + } + + if (definition.isSharedAvailability()) { + WiredVariableReferenceSupport.cacheSharedRoomAssignment(this.room.getId(), definitionItemId, assignment.getValue(), assignment.getUpdatedAt()); + } else { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definitionItemId); + } + + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, assignment.getValue()); + this.broadcastSnapshot(); + return true; + } + + public boolean removeVariable(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + if (definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + Integer previousValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).removeValue(this.room, this.room.getId()); + + if (changed) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.removeSharedRoomVariable((WiredExtraVariableReference) extra); + + if (changed) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + VariableAssignment removed = this.activeAssignmentsByDefinitionId.remove(definitionItemId); + this.deletePersistentAssignment(definitionItemId); + + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definitionItemId); + } + + if (removed != null) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + this.broadcastSnapshot(); + return true; + } + + return false; + } + + public void clearTransientAssignments() { + this.ensurePersistentValuesLoaded(); + + boolean changed = false; + + for (Integer definitionItemId : new ArrayList<>(this.activeAssignmentsByDefinitionId.keySet())) { + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + + if (definition != null && definition.isPermanentAvailability()) { + continue; + } + + if (this.activeAssignmentsByDefinitionId.remove(definitionItemId) != null) { + changed = true; + } + } + + if (changed) { + this.broadcastSnapshot(); + } + } + + public void removeDefinition(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + this.activeAssignmentsByDefinitionId.remove(definitionItemId); + this.deletePersistentAssignment(definitionItemId); + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definitionItemId); + } + this.broadcastSnapshot(); + } + + public void handleDefinitionUpdated(WiredExtraRoomVariable definition) { + if (definition == null) { + return; + } + + this.ensurePersistentValuesLoaded(); + + if (!definition.isPermanentAvailability()) { + this.deletePersistentAssignment(definition.getId()); + } else { + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definition.getId()); + + if (assignment == null) { + return; + } + + this.upsertPersistentAssignment(definition.getId(), assignment); + } + + if (definition.isSharedAvailability()) { + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definition.getId()); + + if (assignment != null) { + WiredVariableReferenceSupport.cacheSharedRoomAssignment(this.room.getId(), definition.getId(), assignment.getValue(), assignment.getUpdatedAt()); + } + } else { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definition.getId()); + } + + this.broadcastSnapshot(); + } + + public Snapshot createSnapshot() { + this.ensurePersistentValuesLoaded(); + + List definitions = new ArrayList<>(); + List assignments = new ArrayList<>(); + List derivedDefinitionIds = new ArrayList<>(); + List roomEchoes = this.getRoomEchoes(); + + for (WiredVariableDefinitionInfo definition : this.getAllDefinitionInfos()) { + definitions.add(new DefinitionEntry(definition.getItemId(), definition.getName(), definition.hasValue(), definition.getAvailability(), definition.isTextConnected(), definition.isReadOnly())); + + if (WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definition.getItemId()) != null) { + derivedDefinitionIds.add(definition.getItemId()); + } + + if (this.isReferenceDefinition(definition.getItemId())) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) this.getDefinitionExtra(definition.getItemId()); + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment(reference); + assignments.add(new AssignmentEntry(definition.getItemId(), (assignment != null) ? assignment.getValue() : 0, 0, (assignment != null) ? assignment.getUpdatedAt() : 0)); + continue; + } + + if (derivedDefinitionIds.contains(definition.getItemId())) { + assignments.add(new AssignmentEntry( + definition.getItemId(), + this.getCurrentValue(definition.getItemId()), + this.getCreatedAt(definition.getItemId()), + this.getUpdatedAt(definition.getItemId()) + )); + continue; + } + + if (roomEchoes.stream().anyMatch(echo -> echo.getId() == definition.getItemId())) { + assignments.add(new AssignmentEntry( + definition.getItemId(), + this.getCurrentValue(definition.getItemId()), + this.getCreatedAt(definition.getItemId()), + this.getUpdatedAt(definition.getItemId()) + )); + continue; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definition.getItemId()); + assignments.add(new AssignmentEntry(definition.getItemId(), (assignment != null) ? assignment.getValue() : 0, 0, (assignment != null) ? assignment.getUpdatedAt() : 0)); + } + + assignments.sort(Comparator.comparingInt(AssignmentEntry::getVariableItemId)); + + return new Snapshot(this.room.getId(), definitions, assignments); + } + + public void sendSnapshot(Habbo habbo) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + return; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.createSnapshot())); + } + + public void broadcastSnapshot() { + RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot(); + RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot(); + Snapshot roomSnapshot = this.createSnapshot(); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + continue; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(userSnapshot, furniSnapshot, roomSnapshot)); + } + } + + public Collection getDefinitions() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet extras = this.room.getRoomSpecialTypes().getExtras(); + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraRoomVariable) { + result.add((WiredExtraRoomVariable) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraRoomVariable::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraRoomVariable::getId)); + return result; + } + + public Collection getAllDefinitionInfos() { + List result = new ArrayList<>(); + List baseDefinitions = new ArrayList<>(); + + for (WiredExtraRoomVariable definition : this.getDefinitions()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + )); + } + + for (WiredExtraVariableReference reference : this.getRoomReferences()) { + baseDefinitions.add(new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly())); + } + + for (WiredExtraVariableEcho echo : this.getRoomEchoes()) { + baseDefinitions.add(echo.createDefinitionInfo(this.room)); + } + + result.addAll(baseDefinitions); + + for (WiredVariableDefinitionInfo definition : baseDefinitions) { + result.addAll(WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, this.getDefinitionExtra(definition.getItemId()), definition)); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public WiredVariableDefinitionInfo getDefinitionInfo(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (extra instanceof WiredExtraRoomVariable) { + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isRoomEcho()) { + return ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + } + + return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + } + + private WiredExtraRoomVariable getDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (!(extra instanceof WiredExtraRoomVariable)) { + return null; + } + + return (WiredExtraRoomVariable) extra; + } + + private InteractionWiredExtra getDefinitionExtra(int definitionItemId) { + if (this.room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return null; + } + + return this.room.getRoomSpecialTypes().getExtra(definitionItemId); + } + + private boolean isReferenceDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + return extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference(); + } + + private List getRoomReferences() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { + result.add((WiredExtraVariableReference) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableReference::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableReference::getId)); + return result; + } + + private List getRoomEchoes() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isRoomEcho()) { + result.add((WiredExtraVariableEcho) extra); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableEcho::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableEcho::getId)); + return result; + } + + private VariableAssignment getRawAssignment(int definitionItemId) { + if (definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment((WiredExtraVariableReference) extra); + return (assignment != null) ? new VariableAssignment(assignment.getValue(), 0, assignment.getUpdatedAt()) : null; + } + + if (extra instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + if (!echo.hasVariable(this.room, this.room.getId())) { + return null; + } + + return new VariableAssignment(echo.getCurrentValue(this.room, this.room.getId()), echo.getCreatedAt(this.room, this.room.getId()), echo.getUpdatedAt(this.room, this.room.getId())); + } + + return this.activeAssignmentsByDefinitionId.get(definitionItemId); + } + + private Integer getRawValue(int definitionItemId) { + VariableAssignment assignment = this.getRawAssignment(definitionItemId); + return (assignment != null) ? assignment.getValue() : null; + } + + private void emitVariableChangedEvents(InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo definitionInfo, Integer previousValue, Integer currentValue) { + if (definitionInfo == null) { + return; + } + + this.emitVariableChangedEvent(definitionInfo.getItemId(), definitionInfo.hasValue(), previousValue, currentValue); + + for (WiredVariableDefinitionInfo derivedDefinition : WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionExtra, definitionInfo)) { + WiredVariableLevelSystemSupport.DerivedDefinition resolvedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, derivedDefinition.getItemId()); + + if (resolvedDefinition == null) { + continue; + } + + Integer derivedPreviousValue = WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), previousValue); + Integer derivedCurrentValue = WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), currentValue); + + this.emitVariableChangedEvent(derivedDefinition.getItemId(), true, derivedPreviousValue, derivedCurrentValue); + } + } + + private void emitVariableChangedEvent(int definitionItemId, boolean hasValue, Integer previousValue, Integer currentValue) { + WiredEvent.VariableChangeKind changeKind = resolveVariableChangeKind(hasValue, previousValue, currentValue); + + if (changeKind == WiredEvent.VariableChangeKind.NONE) { + return; + } + + WiredManager.triggerRoomVariableChanged(this.room, definitionItemId, changeKind); + } + + private static WiredEvent.VariableChangeKind resolveVariableChangeKind(boolean hasValue, Integer previousValue, Integer currentValue) { + if (!hasValue) { + return WiredEvent.VariableChangeKind.NONE; + } + + if (Objects.equals(previousValue, currentValue)) { + return WiredEvent.VariableChangeKind.UNCHANGED; + } + + int previousNumericValue = (previousValue != null) ? previousValue : 0; + int currentNumericValue = (currentValue != null) ? currentValue : 0; + + return (currentNumericValue > previousNumericValue) + ? WiredEvent.VariableChangeKind.INCREASED + : WiredEvent.VariableChangeKind.DECREASED; + } + + private void upsertPersistentAssignment(int definitionItemId, VariableAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.setInt(3, (assignment != null) ? assignment.getValue() : 0); + + int now = Emulator.getIntUnixTimestamp(); + statement.setInt(4, 0); + statement.setInt(5, (assignment != null) ? assignment.getUpdatedAt() : now); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store permanent wired room variable for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + private void deletePersistentAssignment(int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired room variable for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) { + return value; + } + + if (fallback > 0) { + return fallback; + } + return 0; + } + + public static class Snapshot { + private final int roomId; + private final List definitions; + private final List assignments; + + public Snapshot(int roomId, List definitions, List assignments) { + this.roomId = roomId; + this.definitions = definitions; + this.assignments = assignments; + } + + public int getRoomId() { + return this.roomId; + } + + public List getDefinitions() { + return this.definitions; + } + + public List getAssignments() { + return this.assignments; + } + } + + public static class DefinitionEntry { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public DefinitionEntry(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } + + public static class AssignmentEntry { + private final int variableItemId; + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public AssignmentEntry(int variableItemId, Integer value, int createdAt, int updatedAt) { + this.variableItemId = variableItemId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public Integer getValue() { + return this.value; + } + + public boolean hasValue() { + return this.value != null; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static class VariableAssignment { + private int value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(int value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getValue() { + return this.value; + } + + public void setValue(int value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/WiredVariableDefinitionInfo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/WiredVariableDefinitionInfo.java new file mode 100644 index 00000000..26a4a79d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/WiredVariableDefinitionInfo.java @@ -0,0 +1,43 @@ +package com.eu.habbo.habbohotel.rooms; + +public class WiredVariableDefinitionInfo { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public WiredVariableDefinitionInfo(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java index 63fe4f9e..5776b295 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java @@ -38,7 +38,11 @@ public enum WiredConditionType { MATCH_TIME(36), MATCH_DATE(37), ACTOR_DIR(38), - SLC_QUANTITY(39); + SLC_QUANTITY(39), + HAS_VAR(40), + NOT_HAS_VAR(41), + VAR_VAL_MATCH(42), + VAR_AGE_MATCH(43); public final int code; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java index d8755d12..845805a8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java @@ -54,7 +54,12 @@ public enum WiredEffectType { USERS_BY_NAME_SELECTOR(52), USERS_ON_FURNI_SELECTOR(53), USERS_GROUP_SELECTOR(54), - USERS_HANDITEM_SELECTOR(55); + USERS_HANDITEM_SELECTOR(55), + GIVE_VAR(69), + REMOVE_VAR(73), + CHANGE_VAR_VAL(74), + FURNI_WITH_VAR_SELECTOR(75), + USERS_WITH_VAR_SELECTOR(76); public final int code; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java index b9d17bf6..db753325 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java @@ -22,6 +22,7 @@ public enum WiredTriggerType { CLICKS_USER(20), USER_PERFORMS_ACTION(21), CLOCK_COUNTER(22), + VARIABLE_CHANGED(23), SAY_COMMAND(0), IDLES(11), UNIDLES(11), diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java index f52ca70c..0faeee03 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java @@ -59,6 +59,9 @@ public final class WiredContext { /** Extra settings from the trigger item (for legacy compatibility) */ private final Object[] legacySettings; + /** Runtime-local context variables shared through the current execution chain. */ + private WiredContextVariableScope contextVariables; + /** Whether selector item resolution should include wired furniture too. */ private boolean includeWiredSelectorItems = false; @@ -108,6 +111,9 @@ public final class WiredContext { this.services = services; this.state = state; this.legacySettings = legacySettings; + this.contextVariables = (event.getContextVariableScope() != null) + ? event.getContextVariableScope() + : new WiredContextVariableScope(); this.targets = new WiredTargets(); // Default targets: include actor and trigger item for backwards compatibility @@ -259,6 +265,14 @@ public final class WiredContext { return legacySettings != null ? legacySettings : new Object[0]; } + public WiredContextVariableScope contextVariables() { + return this.contextVariables; + } + + public void forkContextVariables() { + this.contextVariables = this.contextVariables.copy(); + } + public boolean includeWiredSelectorItems() { return this.includeWiredSelectorItems; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableScope.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableScope.java new file mode 100644 index 00000000..9ea07467 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableScope.java @@ -0,0 +1,134 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.Emulator; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class WiredContextVariableScope { + private final LinkedHashMap assignments; + + public WiredContextVariableScope() { + this.assignments = new LinkedHashMap<>(); + } + + private WiredContextVariableScope(Map source) { + this.assignments = new LinkedHashMap<>(); + + if (source == null || source.isEmpty()) { + return; + } + + for (Map.Entry entry : source.entrySet()) { + if (entry == null || entry.getKey() == null || entry.getKey() <= 0 || entry.getValue() == null) { + continue; + } + + this.assignments.put(entry.getKey(), entry.getValue().copy()); + } + } + + public WiredContextVariableScope copy() { + return new WiredContextVariableScope(this.assignments); + } + + public boolean hasVariable(int definitionItemId) { + return definitionItemId > 0 && this.assignments.containsKey(definitionItemId); + } + + public Integer getValue(int definitionItemId) { + VariableAssignment assignment = this.assignments.get(definitionItemId); + return assignment != null ? assignment.getValue() : null; + } + + public int getCreatedAt(int definitionItemId) { + VariableAssignment assignment = this.assignments.get(definitionItemId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int definitionItemId) { + VariableAssignment assignment = this.assignments.get(definitionItemId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + public boolean assignValue(int definitionItemId, Integer value, boolean overrideExisting) { + if (definitionItemId <= 0) { + return false; + } + + VariableAssignment existingAssignment = this.assignments.get(definitionItemId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + int now = Emulator.getIntUnixTimestamp(); + + if (existingAssignment == null || overrideExisting) { + this.assignments.put(definitionItemId, new VariableAssignment(value, now, now)); + return true; + } + + return false; + } + + public boolean updateValue(int definitionItemId, Integer value) { + if (definitionItemId <= 0) { + return false; + } + + VariableAssignment assignment = this.assignments.get(definitionItemId); + if (assignment == null) { + return false; + } + + if ((assignment.getValue() == null && value == null) + || (assignment.getValue() != null && assignment.getValue().equals(value))) { + return false; + } + + assignment.setValue(value, Emulator.getIntUnixTimestamp()); + return true; + } + + public boolean removeValue(int definitionItemId) { + if (definitionItemId <= 0) { + return false; + } + + return this.assignments.remove(definitionItemId) != null; + } + + public static final class VariableAssignment { + private Integer value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return this.value; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + + public void setValue(Integer value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + private VariableAssignment copy() { + return new VariableAssignment(this.value, this.createdAt, this.updatedAt); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java new file mode 100644 index 00000000..1c2a2961 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java @@ -0,0 +1,152 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public final class WiredContextVariableSupport { + private WiredContextVariableSupport() { + } + + public static List getDefinitions(Room room) { + List definitions = new ArrayList<>(); + + if (room == null || room.getRoomSpecialTypes() == null) { + return definitions; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(); + if (extras == null || extras.isEmpty()) { + return definitions; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraContextVariable) { + definitions.add((WiredExtraContextVariable) extra); + } + } + + definitions.sort(Comparator + .comparing(WiredExtraContextVariable::getVariableName, String.CASE_INSENSITIVE_ORDER) + .thenComparingInt(WiredExtraContextVariable::getId)); + + return definitions; + } + + public static List createDefinitionInfos(Room room) { + List definitions = new ArrayList<>(); + + for (WiredExtraContextVariable definition : getDefinitions(room)) { + if (definition == null || definition.getVariableName() == null || definition.getVariableName().isEmpty()) { + continue; + } + + definitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + 0, + WiredVariableTextConnectorSupport.isTextConnected(room, definition.getId()), + false)); + } + + return definitions; + } + + public static WiredExtraContextVariable getDefinition(Room room, int definitionItemId) { + if (room == null || room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(definitionItemId); + return (extra instanceof WiredExtraContextVariable) ? (WiredExtraContextVariable) extra : null; + } + + public static WiredVariableDefinitionInfo getDefinitionInfo(Room room, int definitionItemId) { + WiredExtraContextVariable definition = getDefinition(room, definitionItemId); + if (definition == null) { + return null; + } + + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + 0, + WiredVariableTextConnectorSupport.isTextConnected(room, definition.getId()), + false); + } + + public static boolean hasDefinition(Room room, int definitionItemId) { + return getDefinition(room, definitionItemId) != null; + } + + public static boolean assignVariable(WiredContext ctx, Room room, int definitionItemId, Integer value, boolean overrideExisting) { + WiredExtraContextVariable definition = getDefinition(room, definitionItemId); + if (ctx == null || definition == null) { + return false; + } + + if (overrideExisting && ctx.contextVariables().hasVariable(definitionItemId)) { + ctx.forkContextVariables(); + } + + return ctx.contextVariables().assignValue(definitionItemId, definition.hasValue() ? value : null, overrideExisting); + } + + public static boolean updateVariableValue(WiredContext ctx, Room room, int definitionItemId, Integer value) { + WiredExtraContextVariable definition = getDefinition(room, definitionItemId); + if (ctx == null || definition == null || !definition.hasValue()) { + return false; + } + + return ctx.contextVariables().updateValue(definitionItemId, value); + } + + public static boolean removeVariable(WiredContext ctx, Room room, int definitionItemId) { + return ctx != null && getDefinition(room, definitionItemId) != null && ctx.contextVariables().removeValue(definitionItemId); + } + + public static boolean hasVariable(WiredContext ctx, int definitionItemId) { + return ctx != null && ctx.contextVariables().hasVariable(definitionItemId); + } + + public static Integer getCurrentValue(WiredContext ctx, int definitionItemId) { + return ctx != null ? ctx.contextVariables().getValue(definitionItemId) : null; + } + + public static int getCreatedAt(WiredContext ctx, int definitionItemId) { + return ctx != null ? ctx.contextVariables().getCreatedAt(definitionItemId) : 0; + } + + public static int getUpdatedAt(WiredContext ctx, int definitionItemId) { + return ctx != null ? ctx.contextVariables().getUpdatedAt(definitionItemId) : 0; + } + + public static void broadcastDefinitions(Room room) { + if (room == null) { + return; + } + + WiredUserVariablesDataComposer composer = new WiredUserVariablesDataComposer( + room.getUserVariableManager().createSnapshot(), + room.getFurniVariableManager().createSnapshot(), + room.getRoomVariableManager().createSnapshot()); + + room.getHabbos().forEach(habbo -> + { + if (habbo == null || habbo.getClient() == null) { + return; + } + + habbo.getClient().sendResponse(composer); + }); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index db624a5a..10a6f172 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -5,7 +5,9 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; @@ -20,6 +22,7 @@ import com.eu.habbo.habbohotel.wired.WiredConditionOperator; import com.eu.habbo.habbohotel.wired.api.IWiredCondition; import com.eu.habbo.habbohotel.wired.api.IWiredEffect; import com.eu.habbo.habbohotel.wired.api.WiredStack; +import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent; @@ -77,6 +80,33 @@ public final class WiredEngine { /** Duration to ban wired execution in a room after abuse detected (milliseconds) */ public static long WIRED_BAN_DURATION_MS = 600000; + /** Monitor usage window in milliseconds */ + public static int MONITOR_USAGE_WINDOW_MS = 1000; + + /** Monitor execution cap per room window */ + public static int MONITOR_USAGE_LIMIT = 1000; + + /** Maximum delayed events allowed per room at the same time */ + public static int MONITOR_DELAYED_EVENTS_LIMIT = 100; + + /** Average execution threshold that marks overload */ + public static int MONITOR_OVERLOAD_AVERAGE_MS = 50; + + /** Peak execution threshold that marks overload */ + public static int MONITOR_OVERLOAD_PEAK_MS = 150; + + /** Consecutive overloaded windows required before recording overload */ + public static int MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS = 2; + + /** Usage percentage threshold that marks a room as heavy */ + public static int MONITOR_HEAVY_USAGE_PERCENT = 70; + + /** Consecutive windows above threshold before marking heavy */ + public static int MONITOR_HEAVY_CONSECUTIVE_WINDOWS = 5; + + /** Delayed queue percentage threshold that contributes to heavy state */ + public static int MONITOR_HEAVY_DELAYED_PERCENT = 60; + private final WiredServices services; private final WiredStackIndex index; private final int maxStepsPerStack; @@ -93,6 +123,9 @@ public final class WiredEngine { /** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */ private final ConcurrentHashMap bannedRooms; + /** Track monitor diagnostics per room */ + private final ConcurrentHashMap roomDiagnostics; + /** * Create a new wired engine. * @@ -112,6 +145,7 @@ public final class WiredEngine { this.roomRecursionDepth = new ConcurrentHashMap<>(); this.eventRateLimiters = new ConcurrentHashMap<>(); this.bannedRooms = new ConcurrentHashMap<>(); + this.roomDiagnostics = new ConcurrentHashMap<>(); } /** @@ -146,6 +180,12 @@ public final class WiredEngine { // Check and increment recursion depth to prevent infinite loops int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); if (currentDepth >= MAX_RECURSION_DEPTH) { + getDiagnostics(roomId).recordRecursionTimeout( + System.currentTimeMillis(), + String.format("Recursion depth %d/%d while handling %s", currentDepth, MAX_RECURSION_DEPTH, event.getType().name()), + event.getType().name(), + 0 + ); LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " + "Possible infinite loop detected (e.g., collision + chase). Aborting.", roomId, currentDepth); debug(room, "RECURSION LIMIT REACHED - aborting to prevent crash"); @@ -215,9 +255,10 @@ public final class WiredEngine { */ private boolean processStack(WiredStack stack, WiredEvent event, long currentTime) { Room room = event.getRoom(); + WiredTextInputCaptureSupport.CaptureResult captureResult = resolveTextInputCapture(stack, event); // Check if trigger matches - if (!stack.trigger().matches(stack.triggerItem(), event)) { + if (!captureResult.matches()) { return false; } @@ -226,13 +267,23 @@ public final class WiredEngine { return false; } + if (!stackHasExecutableOutcome(stack, event)) { + return false; + } + // Create execution context with stack reference WiredState state = new WiredState(maxStepsPerStack); WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + WiredTextInputCaptureSupport.applyToContext(ctx, room, captureResult); + WiredRoomDiagnostics diagnostics = getDiagnostics(room.getId()); // Initial step for trigger state.step(); - + + int stackCost = estimateStackCost(stack, roomRecursionDepth.getOrDefault(room.getId(), 0)); + String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event); + int monitorSourceId = getMonitorSourceId(stack.triggerItem()); + debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", event.getType(), stack.triggerItem() != null ? stack.triggerItem().getId() : "null", @@ -274,6 +325,16 @@ public final class WiredEngine { return false; } + if (!diagnostics.tryConsumeExecutionBudget( + stackCost, + currentTime, + monitorSourceLabel, + monitorSourceId, + buildStackMonitorReason(stack, event, stackCost))) { + debug(room, "Execution cap blocked stack {}", stack.triggerItem() != null ? stack.triggerItem().getId() : "null"); + return false; + } + if ((event.getType() == WiredEvent.Type.USER_CLICKS_USER) && (stack.triggerItem() instanceof WiredTriggerHabboClicksUser) && event.getActor().isPresent()) { @@ -302,14 +363,22 @@ public final class WiredEngine { // Fire executed event fireExecutedEvent(stack, event); + diagnostics.recordExecution( + state.elapsedMs(), + System.currentTimeMillis(), + monitorSourceLabel, + monitorSourceId, + buildExecutionMonitorReason(stack, state.elapsedMs()) + ); return true; } private boolean wouldTriggerStack(WiredStack stack, WiredEvent event, long currentTime) { Room room = event.getRoom(); + WiredTextInputCaptureSupport.CaptureResult captureResult = resolveTextInputCapture(stack, event); - if (!stack.trigger().matches(stack.triggerItem(), event)) { + if (!captureResult.matches()) { return false; } @@ -317,8 +386,13 @@ public final class WiredEngine { return false; } + if (!stackHasExecutableOutcome(stack, event)) { + return false; + } + WiredState state = new WiredState(maxStepsPerStack); WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + WiredTextInputCaptureSupport.applyToContext(ctx, room, captureResult); state.step(); @@ -336,6 +410,43 @@ public final class WiredEngine { return executionLimitExtra == null || executionLimitExtra.canExecuteAt(currentTime); } + private boolean stackHasExecutableOutcome(WiredStack stack, WiredEvent event) { + if (stack == null) { + return false; + } + + if (stack.hasEffects()) { + return true; + } + + if (stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword) { + return ((WiredTriggerHabboSaysKeyword) stack.triggerItem()).isHideMessage(); + } + + if ((event != null) + && (event.getType() == WiredEvent.Type.USER_CLICKS_USER) + && (stack.triggerItem() instanceof WiredTriggerHabboClicksUser)) { + WiredTriggerHabboClicksUser trigger = (WiredTriggerHabboClicksUser) stack.triggerItem(); + return trigger.isBlockMenuOpen() || trigger.isDoNotRotate(); + } + + return false; + } + + private WiredTextInputCaptureSupport.CaptureResult resolveTextInputCapture(WiredStack stack, WiredEvent event) { + if (stack == null || event == null) { + return WiredTextInputCaptureSupport.CaptureResult.noMatch(); + } + + if (event.getType() != WiredEvent.Type.USER_SAYS || !(stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword)) { + return stack.trigger().matches(stack.triggerItem(), event) + ? WiredTextInputCaptureSupport.CaptureResult.matched(new LinkedHashMap<>()) + : WiredTextInputCaptureSupport.CaptureResult.noMatch(); + } + + return WiredTextInputCaptureSupport.resolve(stack, event); + } + /** * Evaluate all conditions in a stack. */ @@ -462,38 +573,48 @@ public final class WiredEngine { executeOrderedEffects(regulars, ctx, currentTime); return; } else { - // Normal mode: regular effects in random order + // Normal mode: preserve the physical stack order. + // This matches the legacy handler behavior and avoids visual/state races + // for combinations such as Move/Rotate + Match To Snapshot in the same stack. toExecute = new ArrayList<>(regulars); - Collections.shuffle(toExecute); } - // Execute selected effects - for (IWiredEffect effect : toExecute) { - // Check if effect requires actor - if (effect.requiresActor() && !ctx.hasActor()) { - continue; - } + WiredMoveCarryHelper.beginMovementCollection(); - // Handle delay - int delay = effect.getDelay(); - if (delay > 0) { - // Schedule delayed execution - scheduleDelayedEffect(effect, ctx, delay, currentTime); - } else { - // Execute immediately - ctx.state().step(); - try { - effect.execute(ctx); - - // Activate box animation after execution - if (effect instanceof InteractionWiredEffect) { - InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; - wiredEffect.setCooldown(currentTime); - wiredEffect.activateBox(ctx.room(), ctx.actor().orElse(null), currentTime); - } - } catch (Exception e) { - LOGGER.warn("Error executing effect: {}", e.getMessage()); + try { + // Execute selected effects + for (IWiredEffect effect : toExecute) { + // Check if effect requires actor + if (effect.requiresActor() && !ctx.hasActor()) { + continue; } + + // Handle delay + int delay = effect.getDelay(); + if (delay > 0) { + // Schedule delayed execution + scheduleDelayedEffect(effect, ctx, delay, currentTime); + } else { + // Execute immediately + ctx.state().step(); + try { + effect.execute(ctx); + + // Activate box animation after execution + if (effect instanceof InteractionWiredEffect) { + InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; + wiredEffect.setCooldown(currentTime); + wiredEffect.activateBox(ctx.room(), ctx.actor().orElse(null), currentTime); + } + } catch (Exception e) { + LOGGER.warn("Error executing effect: {}", e.getMessage()); + } + } + } + } finally { + ServerMessage movementComposer = WiredMoveCarryHelper.finishMovementCollection(); + if (movementComposer != null) { + ctx.room().sendComposer(movementComposer); } } } @@ -561,21 +682,50 @@ public final class WiredEngine { int furniLimit = Integer.MAX_VALUE; int userLimit = Integer.MAX_VALUE; + List furniVariableFilters = new ArrayList<>(); + List userVariableFilters = new ArrayList<>(); for (InteractionWiredExtra extra : extras) { if (extra instanceof WiredExtraFilterFurni) { furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); } else if (extra instanceof WiredExtraFilterUser) { userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); + } else if (extra instanceof WiredExtraFilterFurniByVariable) { + furniVariableFilters.add((WiredExtraFilterFurniByVariable) extra); + } else if (extra instanceof WiredExtraFilterUsersByVariable) { + userVariableFilters.add((WiredExtraFilterUsersByVariable) extra); } } - if (ctx.targets().isItemsModifiedBySelector() && furniLimit != Integer.MAX_VALUE) { - ctx.targets().setItems(limitIterable(ctx.targets().items(), furniLimit)); + furniVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + userVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + + if (ctx.targets().isItemsModifiedBySelector()) { + Iterable filteredItems = ctx.targets().items(); + + for (WiredExtraFilterFurniByVariable extra : furniVariableFilters) { + filteredItems = extra.filterItems(room, ctx, filteredItems); + } + + if (furniLimit != Integer.MAX_VALUE) { + filteredItems = limitIterable(filteredItems, furniLimit); + } + + ctx.targets().setItems(filteredItems); } - if (ctx.targets().isUsersModifiedBySelector() && userLimit != Integer.MAX_VALUE) { - ctx.targets().setUsers(limitIterable(ctx.targets().users(), userLimit)); + if (ctx.targets().isUsersModifiedBySelector()) { + Iterable filteredUsers = ctx.targets().users(); + + for (WiredExtraFilterUsersByVariable extra : userVariableFilters) { + filteredUsers = extra.filterUsers(room, ctx, filteredUsers); + } + + if (userLimit != Integer.MAX_VALUE) { + filteredUsers = limitIterable(filteredUsers, userLimit); + } + + ctx.targets().setUsers(filteredUsers); } } @@ -604,6 +754,19 @@ public final class WiredEngine { * Schedule a delayed effect execution. */ private void scheduleDelayedEffect(IWiredEffect effect, WiredContext ctx, int delay, long triggerTime) { + WiredRoomDiagnostics diagnostics = getDiagnostics(ctx.room().getId()); + String sourceLabel = getMonitorSourceLabel(ctx.triggerItem(), ctx.event()); + int sourceId = getMonitorSourceId(ctx.triggerItem()); + + if (!diagnostics.tryScheduleDelayedEvent( + System.currentTimeMillis(), + sourceLabel, + sourceId, + String.format("Scheduling delayed effect %s with delay %d tick(s)", effect.getClass().getSimpleName(), delay))) { + debug(ctx.room(), "Delayed events cap blocked effect {}", effect.getClass().getSimpleName()); + return; + } + // Delay is in 500ms ticks long delayMs = delay * 500L; long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - triggerTime); @@ -613,6 +776,7 @@ public final class WiredEngine { Emulator.getThreading().run(() -> { if (!room.isLoaded() || room.getHabbos().isEmpty()) { + diagnostics.completeDelayedEvent(); return; } @@ -627,6 +791,8 @@ public final class WiredEngine { } } catch (Exception e) { LOGGER.warn("Error executing delayed effect: {}", e.getMessage()); + } finally { + diagnostics.completeDelayedEvent(); } }, remainingDelayMs); } @@ -712,6 +878,19 @@ public final class WiredEngine { } private void scheduleOrderedEffectBatch(List batch, WiredContext ctx, int delay, long triggerTime) { + WiredRoomDiagnostics diagnostics = getDiagnostics(ctx.room().getId()); + String sourceLabel = getMonitorSourceLabel(ctx.triggerItem(), ctx.event()); + int sourceId = getMonitorSourceId(ctx.triggerItem()); + + if (!diagnostics.tryScheduleDelayedEvent( + System.currentTimeMillis(), + sourceLabel, + sourceId, + String.format("Scheduling ordered batch with %d effect(s) and delay %d tick(s)", batch.size(), delay))) { + debug(ctx.room(), "Delayed events cap blocked ordered batch with {} effect(s)", batch.size()); + return; + } + long delayMs = delay * 500L; long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - triggerTime); long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger); @@ -719,10 +898,15 @@ public final class WiredEngine { Emulator.getThreading().run(() -> { if (!room.isLoaded() || room.getHabbos().isEmpty()) { + diagnostics.completeDelayedEvent(); return; } - executeOrderedEffectBatch(batch, ctx, System.currentTimeMillis(), true); + try { + executeOrderedEffectBatch(batch, ctx, System.currentTimeMillis(), true); + } finally { + diagnostics.completeDelayedEvent(); + } }, remainingDelayMs); } @@ -730,21 +914,30 @@ public final class WiredEngine { Room room = ctx.room(); RoomUnit actor = ctx.actor().orElse(null); - for (IWiredEffect effect : batch) { - try { - if (!useExecutionTimeForCooldown) { - ctx.state().step(); - } + WiredMoveCarryHelper.beginMovementCollection(); - effect.execute(ctx); + try { + for (IWiredEffect effect : batch) { + try { + if (!useExecutionTimeForCooldown) { + ctx.state().step(); + } - if (effect instanceof InteractionWiredEffect) { - InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; - wiredEffect.setCooldown(executionTime); - wiredEffect.activateBox(room, actor, executionTime); + effect.execute(ctx); + + if (effect instanceof InteractionWiredEffect) { + InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; + wiredEffect.setCooldown(executionTime); + wiredEffect.activateBox(room, actor, executionTime); + } + } catch (Exception e) { + LOGGER.warn("Error executing ordered effect batch item: {}", e.getMessage()); } - } catch (Exception e) { - LOGGER.warn("Error executing ordered effect batch item: {}", e.getMessage()); + } + } finally { + ServerMessage movementComposer = WiredMoveCarryHelper.finishMovementCollection(); + if (movementComposer != null) { + room.sendComposer(movementComposer); } } } @@ -961,6 +1154,29 @@ public final class WiredEngine { String prefix = roomId + ":"; eventRateLimiters.keySet().removeIf(key -> key.startsWith(prefix)); } + + /** + * Clear monitor diagnostics for a specific room. + * @param roomId the room ID + */ + public void clearRoomDiagnostics(int roomId) { + roomDiagnostics.remove(roomId); + } + + /** + * Clear all monitor diagnostics. + */ + public void clearAllDiagnostics() { + roomDiagnostics.clear(); + } + + public void clearRoomDiagnosticsLogs(int roomId) { + WiredRoomDiagnostics diagnostics = roomDiagnostics.get(roomId); + + if (diagnostics != null) { + diagnostics.clearLogs(); + } + } /** * Clear room ban for a specific room. @@ -970,6 +1186,23 @@ public final class WiredEngine { public void clearRoomBan(int roomId) { bannedRooms.remove(roomId); } + + /** + * Get a monitor snapshot for a room. + * @param roomId the room ID + * @return the diagnostics snapshot + */ + public WiredRoomDiagnostics.Snapshot getDiagnosticsSnapshot(int roomId) { + long now = System.currentTimeMillis(); + long killedUntil = bannedRooms.getOrDefault(roomId, 0L); + + return getDiagnostics(roomId).snapshot( + getRecursionDepth(roomId), + MAX_RECURSION_DEPTH, + killedUntil, + now + ); + } /** * Check if a room is currently banned from wired execution. @@ -997,9 +1230,15 @@ public final class WiredEngine { * @param roomId the room ID * @param room the room object (for sending alerts) */ - private void banRoom(int roomId, Room room) { + private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) { long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS; bannedRooms.put(roomId, banExpiry); + getDiagnostics(roomId).recordKilled( + System.currentTimeMillis(), + String.format("Rate limit exceeded for %s with %d event(s) in %dms", eventType.name(), eventCount, RATE_LIMIT_WINDOW_MS), + eventType.name(), + 0 + ); long banMinutes = WIRED_BAN_DURATION_MS / 60000; @@ -1049,10 +1288,109 @@ public final class WiredEngine { boolean limited = tracker.isRateLimited(now); if (limited && tracker.shouldBan(now)) { // First time hitting limit in this suppression window - ban the room - banRoom(roomId, room); + banRoom(roomId, room, eventType, tracker.getEventCount()); } return limited; } + + private WiredRoomDiagnostics getDiagnostics(int roomId) { + return roomDiagnostics.computeIfAbsent(roomId, ignored -> new WiredRoomDiagnostics( + MONITOR_USAGE_WINDOW_MS, + MONITOR_USAGE_LIMIT, + MONITOR_DELAYED_EVENTS_LIMIT, + MONITOR_OVERLOAD_AVERAGE_MS, + MONITOR_OVERLOAD_PEAK_MS, + MONITOR_HEAVY_USAGE_PERCENT, + MONITOR_HEAVY_CONSECUTIVE_WINDOWS, + MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS, + MONITOR_HEAVY_DELAYED_PERCENT, + 200 + )); + } + + private int estimateStackCost(WiredStack stack, int recursionDepth) { + int cost = 1; + + if (stack == null) { + return cost; + } + + cost += Math.max(0, stack.conditions().size()); + + for (IWiredEffect effect : stack.effects()) { + if (effect == null) { + continue; + } + + cost += effect.isSelector() ? 2 : 3; + + if (effect.getDelay() > 0) { + cost += 4; + } + } + + cost += Math.max(0, recursionDepth) * 2; + + return Math.max(1, cost); + } + + private String getMonitorSourceLabel(HabboItem triggerItem, WiredEvent event) { + if (triggerItem != null && triggerItem.getBaseItem() != null && triggerItem.getBaseItem().getInteractionType() != null) { + return triggerItem.getBaseItem().getInteractionType().getName(); + } + + return (event != null && event.getType() != null) ? event.getType().name() : "room"; + } + + private int getMonitorSourceId(HabboItem triggerItem) { + return triggerItem != null ? triggerItem.getId() : 0; + } + + private String buildStackMonitorReason(WiredStack stack, WiredEvent event, int stackCost) { + if (stack == null) { + return String.format("Processing %s with estimated cost %d", event.getType().name(), stackCost); + } + + int selectors = 0; + int delayedEffects = 0; + + for (IWiredEffect effect : stack.effects()) { + if (effect == null) { + continue; + } + + if (effect.isSelector()) { + selectors++; + } + + if (effect.getDelay() > 0) { + delayedEffects++; + } + } + + return String.format( + "Trigger %s with %d condition(s), %d effect(s), %d selector(s), %d delayed effect(s) and estimated cost %d", + event.getType().name(), + stack.conditions().size(), + stack.effects().size(), + selectors, + delayedEffects, + stackCost + ); + } + + private String buildExecutionMonitorReason(WiredStack stack, long elapsedMs) { + if (stack == null) { + return String.format("Execution completed in %dms", elapsedMs); + } + + return String.format( + "Stack with %d condition(s) and %d effect(s) completed in %dms", + stack.conditions().size(), + stack.effects().size(), + elapsedMs + ); + } /** * Tracks event rate for a specific room + event type combination. @@ -1094,5 +1432,9 @@ public final class WiredEngine { } return false; } + + synchronized int getEventCount() { + return eventCount; + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java index fb194c2c..0a18ee86 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java @@ -66,6 +66,9 @@ public final class WiredEvent { /** Up-counter reaches a configured elapsed time */ CLOCK_COUNTER_REACHED(WiredTriggerType.CLOCK_COUNTER), + + /** A user, furni or global variable changed */ + VARIABLE_CHANGED(WiredTriggerType.VARIABLE_CHANGED), /** Long timer repeat */ TIMER_REPEAT_LONG(WiredTriggerType.PERIODICALLY_LONG), @@ -150,6 +153,13 @@ public final class WiredEvent { } } + public enum VariableChangeKind { + NONE, + INCREASED, + DECREASED, + UNCHANGED + } + private final Type type; private final Room room; private final RoomUnit actor; // nullable - the user/bot that caused the event @@ -164,6 +174,16 @@ public final class WiredEvent { private final int signalChannel; // channel for signal routing (0-based) private final int actionId; // user action id for USER_PERFORMS_ACTION private final int actionParameter; // sign/dance parameter when relevant + private final int chatType; // RoomChatType metadata for USER_SAYS + private final int chatStyle; // bubble style for USER_SAYS + private final int signalUserCount; // forwarded users in SIGNAL_RECEIVED + private final int signalFurniCount; // forwarded furni in SIGNAL_RECEIVED + private final int variableTargetType; + private final int variableDefinitionItemId; + private final boolean variableCreated; + private final boolean variableDeleted; + private final VariableChangeKind variableChangeKind; + private final WiredContextVariableScope contextVariableScope; private final long createdAtMs; private WiredEvent(Builder builder) { @@ -181,6 +201,16 @@ public final class WiredEvent { this.signalChannel = builder.signalChannel; this.actionId = builder.actionId; this.actionParameter = builder.actionParameter; + this.chatType = builder.chatType; + this.chatStyle = builder.chatStyle; + this.signalUserCount = builder.signalUserCount; + this.signalFurniCount = builder.signalFurniCount; + this.variableTargetType = builder.variableTargetType; + this.variableDefinitionItemId = builder.variableDefinitionItemId; + this.variableCreated = builder.variableCreated; + this.variableDeleted = builder.variableDeleted; + this.variableChangeKind = builder.variableChangeKind; + this.contextVariableScope = builder.contextVariableScope; this.createdAtMs = builder.createdAtMs; } @@ -291,6 +321,46 @@ public final class WiredEvent { return actionParameter; } + public int getChatType() { + return chatType; + } + + public int getChatStyle() { + return chatStyle; + } + + public int getSignalUserCount() { + return signalUserCount; + } + + public int getSignalFurniCount() { + return signalFurniCount; + } + + public int getVariableTargetType() { + return variableTargetType; + } + + public int getVariableDefinitionItemId() { + return variableDefinitionItemId; + } + + public boolean isVariableCreated() { + return variableCreated; + } + + public boolean isVariableDeleted() { + return variableDeleted; + } + + public VariableChangeKind getVariableChangeKind() { + return variableChangeKind; + } + + public WiredContextVariableScope getContextVariableScope() { + return contextVariableScope; + } + /** * Get the timestamp when this event was created. * @return milliseconds since epoch @@ -348,6 +418,16 @@ public final class WiredEvent { private int signalChannel; private int actionId; private int actionParameter = -1; + private int chatType = -1; + private int chatStyle = -1; + private int signalUserCount; + private int signalFurniCount; + private int variableTargetType = -1; + private int variableDefinitionItemId; + private boolean variableCreated; + private boolean variableDeleted; + private VariableChangeKind variableChangeKind = VariableChangeKind.NONE; + private WiredContextVariableScope contextVariableScope; private long createdAtMs = System.currentTimeMillis(); private Builder(Type type, Room room) { @@ -462,6 +542,56 @@ public final class WiredEvent { return this; } + public Builder chatType(int chatType) { + this.chatType = chatType; + return this; + } + + public Builder chatStyle(int chatStyle) { + this.chatStyle = chatStyle; + return this; + } + + public Builder signalUserCount(int signalUserCount) { + this.signalUserCount = Math.max(0, signalUserCount); + return this; + } + + public Builder signalFurniCount(int signalFurniCount) { + this.signalFurniCount = Math.max(0, signalFurniCount); + return this; + } + + public Builder variableTargetType(int variableTargetType) { + this.variableTargetType = variableTargetType; + return this; + } + + public Builder variableDefinitionItemId(int variableDefinitionItemId) { + this.variableDefinitionItemId = Math.max(0, variableDefinitionItemId); + return this; + } + + public Builder variableCreated(boolean variableCreated) { + this.variableCreated = variableCreated; + return this; + } + + public Builder variableDeleted(boolean variableDeleted) { + this.variableDeleted = variableDeleted; + return this; + } + + public Builder variableChangeKind(VariableChangeKind variableChangeKind) { + this.variableChangeKind = (variableChangeKind != null) ? variableChangeKind : VariableChangeKind.NONE; + return this; + } + + public Builder contextVariableScope(WiredContextVariableScope contextVariableScope) { + this.contextVariableScope = contextVariableScope; + return this; + } + /** * Set a custom creation timestamp. * @param createdAtMs milliseconds since epoch diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java new file mode 100644 index 00000000..c1fe88b4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java @@ -0,0 +1,554 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboGender; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.Locale; + +public final class WiredInternalVariableSupport { + private WiredInternalVariableSupport() { + } + + public static String normalizeKey(String key) { + if (key == null) { + return ""; + } + + String normalized = key.trim(); + + return switch (normalized) { + case "@position.x" -> "@position_x"; + case "@position.y" -> "@position_y"; + case "@effect" -> "@effect_id"; + case "@handitems" -> "@handitem_id"; + case "@is_mute" -> "@is_muted"; + case "@teams.red.score" -> "@team_red_score"; + case "@teams.green.score" -> "@team_green_score"; + case "@teams.blue.score" -> "@team_blue_score"; + case "@teams.yellow.score" -> "@team_yellow_score"; + case "@teams.red.size" -> "@team_red_size"; + case "@teams.green.size" -> "@team_green_size"; + case "@teams.blue.size" -> "@team_blue_size"; + case "@teams.yellow.size" -> "@team_yellow_size"; + default -> normalized; + }; + } + + public static boolean canUseUserDestination(String key) { + String normalized = normalizeKey(key); + return "@position_x".equals(normalized) || "@position_y".equals(normalized) || "@direction".equals(normalized); + } + + public static boolean canUseFurniDestination(String key) { + String normalized = normalizeKey(key); + return "@state".equals(normalized) || "@position_x".equals(normalized) || "@position_y".equals(normalized) + || "@rotation".equals(normalized) || "@altitude".equals(normalized); + } + + public static boolean canUseUserReference(String key) { + String normalized = normalizeKey(key); + + return "@index".equals(normalized) || "@type".equals(normalized) || "@gender".equals(normalized) + || "@level".equals(normalized) || "@achievement_score".equals(normalized) || "@is_hc".equals(normalized) + || "@has_rights".equals(normalized) || "@is_group_admin".equals(normalized) || "@is_owner".equals(normalized) + || "@is_muted".equals(normalized) || "@is_trading".equals(normalized) || "@is_frozen".equals(normalized) + || "@effect_id".equals(normalized) || "@team_score".equals(normalized) || "@team_color".equals(normalized) + || "@team_type".equals(normalized) || "@sign".equals(normalized) || "@dance".equals(normalized) + || "@is_idle".equals(normalized) || "@handitem_id".equals(normalized) || "@position_x".equals(normalized) + || "@position_y".equals(normalized) || "@direction".equals(normalized) || "@altitude".equals(normalized) + || "@favourite_group_id".equals(normalized) || "@room_entry.method".equals(normalized) + || "@room_entry.teleport_id".equals(normalized) || "@user_id".equals(normalized) + || "@bot_id".equals(normalized) || "@pet_id".equals(normalized) || "@pet_owner_id".equals(normalized); + } + + public static boolean canUseFurniReference(String key) { + String normalized = normalizeKey(key); + + return "~teleport.target_id".equals(normalized) || "@id".equals(normalized) || "@class_id".equals(normalized) + || "@height".equals(normalized) || "@state".equals(normalized) || "@position_x".equals(normalized) + || "@position_y".equals(normalized) || "@rotation".equals(normalized) || "@altitude".equals(normalized) + || "@is_invisible".equals(normalized) || "@type".equals(normalized) || "@is_stackable".equals(normalized) + || "@can_stand_on".equals(normalized) || "@can_sit_on".equals(normalized) || "@can_lay_on".equals(normalized) + || "@owner_id".equals(normalized) || "@wallitem_offset".equals(normalized) + || "@dimensions.x".equals(normalized) || "@dimensions.y".equals(normalized); + } + + public static boolean canUseRoomReference(String key) { + String normalized = normalizeKey(key); + + return "@furni_count".equals(normalized) || "@user_count".equals(normalized) || "@wired_timer".equals(normalized) + || "@team_red_score".equals(normalized) || "@team_green_score".equals(normalized) || "@team_blue_score".equals(normalized) + || "@team_yellow_score".equals(normalized) || "@team_red_size".equals(normalized) || "@team_green_size".equals(normalized) + || "@team_blue_size".equals(normalized) || "@team_yellow_size".equals(normalized) || "@room_id".equals(normalized) + || "@group_id".equals(normalized) || "@timezone_server".equals(normalized) || "@timezone_client".equals(normalized) + || "@current_time".equals(normalized) || "@current_time.millisecond_of_second".equals(normalized) + || "@current_time.seconds_of_minute".equals(normalized) || "@current_time.minute_of_hour".equals(normalized) + || "@current_time.hour_of_day".equals(normalized) || "@current_time.day_of_week".equals(normalized) + || "@current_time.day_of_month".equals(normalized) || "@current_time.day_of_year".equals(normalized) + || "@current_time.week_of_year".equals(normalized) || "@current_time.month_of_year".equals(normalized) + || "@current_time.year".equals(normalized); + } + + public static boolean canUseContextReference(String key) { + String normalized = normalizeKey(key); + + return "@selector_furni_count".equals(normalized) || "@selector_user_count".equals(normalized) + || "@signal_furni_count".equals(normalized) || "@signal_user_count".equals(normalized) + || "@antenna_id".equals(normalized) || "@chat_type".equals(normalized) || "@chat_style".equals(normalized); + } + + public static boolean hasUserValue(Room room, RoomUnit roomUnit, String key) { + if (room == null || roomUnit == null) { + return false; + } + + Habbo habbo = room.getHabbo(roomUnit); + Bot bot = room.getBot(roomUnit); + Pet pet = room.getPet(roomUnit); + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@index", "@type", "@level", "@achievement_score", "@position_x", "@position_y", "@direction", "@altitude" -> true; + case "@gender" -> habbo != null || bot != null; + case "@is_hc" -> habbo != null && habbo.getHabboStats().hasActiveClub(); + case "@has_rights" -> habbo != null && (room.hasRights(habbo) || room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS)); + case "@is_group_admin" -> habbo != null && room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_ADMIN); + case "@is_owner" -> habbo != null && room.isOwner(habbo); + case "@is_muted" -> (habbo != null && room.isMuted(habbo)) || (pet != null && pet.isMuted()); + case "@is_trading" -> habbo != null && room.getActiveTradeForHabbo(habbo) != null; + case "@is_frozen" -> WiredFreezeUtil.isFrozen(roomUnit); + case "@effect_id" -> roomUnit.getEffectId() > 0; + case "@team_score", "@team_color", "@team_type" -> getTeamEffectData(roomUnit.getEffectId()) != null; + case "@sign" -> roomUnit.hasStatus(RoomUnitStatus.SIGN); + case "@dance" -> roomUnit.getDanceType() != null && roomUnit.getDanceType() != DanceType.NONE; + case "@is_idle" -> roomUnit.isIdle(); + case "@handitem_id" -> roomUnit.getHandItem() > 0; + case "@favourite_group_id" -> habbo != null && habbo.getHabboStats().guild > 0; + case "@room_entry.method" -> habbo != null && hasRoomEntryMethod(habbo); + case "@room_entry.teleport_id" -> habbo != null && habbo.getHabboInfo().getRoomEntryTeleportId() > 0; + case "@user_id" -> habbo != null; + case "@bot_id" -> bot != null; + case "@pet_id" -> pet != null; + case "@pet_owner_id" -> pet != null && pet.getUserId() > 0; + default -> false; + }; + } + + public static boolean hasFurniValue(HabboItem item, String key) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@id", "@class_id", "@height", "@state", "@position_x", "@position_y", "@rotation", "@altitude", + "@is_invisible", "@type", "@owner_id", "@dimensions.x", "@dimensions.y" -> true; + case "~teleport.target_id" -> item.getTeleportTargetId() > 0; + case "@wallitem_offset" -> item.getBaseItem().getType() == FurnitureType.WALL; + case "@is_stackable" -> item.getBaseItem().allowStack(); + case "@can_stand_on" -> item.getBaseItem().allowWalk(); + case "@can_sit_on" -> item.getBaseItem().allowSit(); + case "@can_lay_on" -> item.getBaseItem().allowLay(); + default -> false; + }; + } + + public static boolean hasRoomValue(Room room, String key) { + return room != null && canUseRoomReference(key); + } + + public static Integer readUserValue(Room room, RoomUnit roomUnit, String key) { + if (room == null || roomUnit == null) { + return null; + } + + Habbo habbo = room.getHabbo(roomUnit); + Bot bot = room.getBot(roomUnit); + Pet pet = room.getPet(roomUnit); + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@index" -> roomUnit.getId(); + case "@type" -> getUserTypeValue(habbo, bot, pet); + case "@gender" -> getGenderValue(habbo, bot); + case "@level" -> (roomUnit.getRightsLevel() != null) ? roomUnit.getRightsLevel().level : 0; + case "@achievement_score" -> (habbo != null) ? habbo.getHabboStats().getAchievementScore() : null; + case "@is_hc" -> (habbo != null && habbo.getHabboStats().hasActiveClub()) ? 1 : 0; + case "@has_rights" -> (habbo != null && (room.hasRights(habbo) || room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS))) ? 1 : 0; + case "@is_group_admin" -> (habbo != null && room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_ADMIN)) ? 1 : 0; + case "@is_owner" -> (habbo != null && room.isOwner(habbo)) ? 1 : 0; + case "@is_muted" -> ((habbo != null && room.isMuted(habbo)) || (pet != null && pet.isMuted())) ? 1 : 0; + case "@is_trading" -> (habbo != null && room.getActiveTradeForHabbo(habbo) != null) ? 1 : 0; + case "@is_frozen" -> WiredFreezeUtil.isFrozen(roomUnit) ? 1 : 0; + case "@effect_id" -> roomUnit.getEffectId(); + case "@team_score" -> getUserTeamScore(room, habbo); + case "@team_color" -> getTeamColorId(roomUnit.getEffectId()); + case "@team_type" -> getTeamTypeId(roomUnit.getEffectId()); + case "@sign" -> parseStatusInteger(roomUnit, RoomUnitStatus.SIGN); + case "@dance" -> (roomUnit.getDanceType() != null) ? roomUnit.getDanceType().getType() : 0; + case "@is_idle" -> roomUnit.isIdle() ? 1 : 0; + case "@handitem_id" -> roomUnit.getHandItem(); + case "@position_x" -> (int) roomUnit.getX(); + case "@position_y" -> (int) roomUnit.getY(); + case "@direction" -> (roomUnit.getBodyRotation() != null) ? (int) roomUnit.getBodyRotation().getValue() : 0; + case "@altitude" -> (int) Math.round(roomUnit.getZ() * 100); + case "@favourite_group_id" -> (habbo != null) ? habbo.getHabboStats().guild : null; + case "@room_entry.method" -> getRoomEntryMethodValue(habbo); + case "@room_entry.teleport_id" -> (habbo != null) ? habbo.getHabboInfo().getRoomEntryTeleportId() : null; + case "@user_id" -> (habbo != null) ? habbo.getHabboInfo().getId() : null; + case "@bot_id" -> (bot != null) ? bot.getId() : null; + case "@pet_id" -> (pet != null) ? pet.getId() : null; + case "@pet_owner_id" -> (pet != null) ? pet.getUserId() : null; + default -> null; + }; + } + + public static boolean writeUserValue(Room room, RoomUnit roomUnit, String key, int value) { + if (room == null || roomUnit == null) { + return false; + } + + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@position_x" -> moveUserTo(room, roomUnit, value, roomUnit.getY()); + case "@position_y" -> moveUserTo(room, roomUnit, roomUnit.getX(), value); + case "@direction" -> { + RoomUserRotation rotation = RoomUserRotation.fromValue(value); + yield WiredUserMovementHelper.updateUserDirection(room, roomUnit, rotation, rotation); + } + default -> false; + }; + } + + public static Integer readFurniValue(Room room, HabboItem item, String key) { + if (room == null || item == null) { + return null; + } + + String normalized = normalizeKey(key); + + return switch (normalized) { + case "~teleport.target_id" -> item.getTeleportTargetId(); + case "@id" -> item.getId(); + case "@class_id" -> (item.getBaseItem() != null) ? item.getBaseItem().getId() : null; + case "@height" -> (item.getBaseItem() != null) ? (int) Math.round(item.getBaseItem().getHeight() * 100) : null; + case "@state" -> parseInteger(item.getExtradata()); + case "@position_x" -> (int) item.getX(); + case "@position_y" -> (int) item.getY(); + case "@rotation" -> item.getRotation(); + case "@altitude" -> (int) Math.round(item.getZ() * 100); + case "@is_invisible" -> 0; + case "@type" -> 0; + case "@is_stackable" -> (item.getBaseItem() != null && item.getBaseItem().allowStack()) ? 1 : 0; + case "@can_stand_on" -> (item.getBaseItem() != null && item.getBaseItem().allowWalk()) ? 1 : 0; + case "@can_sit_on" -> (item.getBaseItem() != null && item.getBaseItem().allowSit()) ? 1 : 0; + case "@can_lay_on" -> (item.getBaseItem() != null && item.getBaseItem().allowLay()) ? 1 : 0; + case "@wallitem_offset" -> ((item.getBaseItem() != null) && item.getBaseItem().getType() == FurnitureType.WALL && item.getWallPosition() != null && !item.getWallPosition().trim().isEmpty()) ? 1 : 0; + case "@dimensions.x" -> (item.getBaseItem() != null) ? (int) item.getBaseItem().getWidth() : null; + case "@dimensions.y" -> (item.getBaseItem() != null) ? (int) item.getBaseItem().getLength() : null; + case "@owner_id" -> item.getUserId(); + default -> null; + }; + } + + public static boolean writeFurniValue(Room room, HabboItem item, String key, int value) { + if (room == null || item == null) { + return false; + } + + String normalized = normalizeKey(key); + + if ("@state".equals(normalized)) { + item.setExtradata(String.valueOf(value)); + room.updateItemState(item); + return true; + } + + if (item.getBaseItem() == null || item.getBaseItem().getType() != FurnitureType.FLOOR) { + return false; + } + + return switch (normalized) { + case "@position_x" -> moveFurniTo(room, item, value, item.getY(), item.getRotation(), item.getZ()); + case "@position_y" -> moveFurniTo(room, item, item.getX(), value, item.getRotation(), item.getZ()); + case "@rotation" -> moveFurniTo(room, item, item.getX(), item.getY(), value, item.getZ()); + case "@altitude" -> moveFurniTo(room, item, item.getX(), item.getY(), item.getRotation(), value / 100.0); + default -> false; + }; + } + + public static Integer readRoomValue(Room room, String key) { + if (room == null) { + return null; + } + + ZonedDateTime now = HotelDateTimeUtil.now(); + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@furni_count" -> room.getFloorItems().size() + room.getWallItems().size(); + case "@user_count" -> room.getUserCount(); + case "@wired_timer" -> (int) (WiredManager.getTickService().getTickCount() / 10L); + case "@team_red_score" -> getTeamMetric(room, GameTeamColors.RED, true); + case "@team_green_score" -> getTeamMetric(room, GameTeamColors.GREEN, true); + case "@team_blue_score" -> getTeamMetric(room, GameTeamColors.BLUE, true); + case "@team_yellow_score" -> getTeamMetric(room, GameTeamColors.YELLOW, true); + case "@team_red_size" -> getTeamMetric(room, GameTeamColors.RED, false); + case "@team_green_size" -> getTeamMetric(room, GameTeamColors.GREEN, false); + case "@team_blue_size" -> getTeamMetric(room, GameTeamColors.BLUE, false); + case "@team_yellow_size" -> getTeamMetric(room, GameTeamColors.YELLOW, false); + case "@room_id" -> room.getId(); + case "@group_id" -> room.getGuildId(); + case "@timezone_server" -> now.getOffset().getTotalSeconds() / 60; + case "@timezone_client" -> 0; + case "@current_time" -> (int) now.toEpochSecond(); + case "@current_time.millisecond_of_second" -> now.getNano() / 1_000_000; + case "@current_time.seconds_of_minute" -> now.getSecond(); + case "@current_time.minute_of_hour" -> now.getMinute(); + case "@current_time.hour_of_day" -> now.getHour(); + case "@current_time.day_of_week" -> now.getDayOfWeek().getValue(); + case "@current_time.day_of_month" -> now.getDayOfMonth(); + case "@current_time.day_of_year" -> now.getDayOfYear(); + case "@current_time.week_of_year" -> now.get(WeekFields.of(Locale.ITALY).weekOfWeekBasedYear()); + case "@current_time.month_of_year" -> now.getMonthValue(); + case "@current_time.year" -> now.getYear(); + default -> null; + }; + } + + public static Integer readContextValue(WiredContext ctx, String key) { + if (ctx == null) { + return null; + } + + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@selector_furni_count" -> countIterable(ctx.targets() != null ? ctx.targets().items() : null); + case "@selector_user_count" -> countIterable(ctx.targets() != null ? ctx.targets().users() : null); + case "@signal_furni_count" -> ctx.event().getSignalFurniCount(); + case "@signal_user_count" -> ctx.event().getSignalUserCount(); + case "@antenna_id" -> ctx.event().getSignalChannel(); + case "@chat_type" -> ctx.event().getChatType(); + case "@chat_style" -> ctx.event().getChatStyle(); + default -> null; + }; + } + + private static Integer getUserTypeValue(Habbo habbo, Bot bot, Pet pet) { + if (habbo != null) return 1; + if (pet != null) return 2; + if (bot != null) return 4; + return null; + } + + private static Integer getGenderValue(Habbo habbo, Bot bot) { + HabboGender gender = null; + + if (habbo != null && habbo.getHabboInfo() != null) { + gender = habbo.getHabboInfo().getGender(); + } else if (bot != null) { + gender = bot.getGender(); + } + + if (gender == null) { + return -1; + } + + return (gender == HabboGender.F) ? 1 : 0; + } + + private static Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getGamePlayer() == null) { + return null; + } + + Game game = resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) { + return gamePlayer.getScore(); + } + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private static Integer getTeamColorId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return (data != null) ? data.colorId : null; + } + + private static Integer getTeamTypeId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return (data != null) ? data.typeId : null; + } + + private static TeamEffectData getTeamEffectData(int effectId) { + if (effectId <= 0) { + return null; + } + + if (effectId >= 223 && effectId <= 226) return new TeamEffectData(effectId - 222, 0); + if (effectId >= 33 && effectId <= 36) return new TeamEffectData(effectId - 32, 1); + if (effectId >= 40 && effectId <= 43) return new TeamEffectData(effectId - 39, 2); + + return null; + } + + private static int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = resolveTeamGame(room, null); + if (game == null || color == null) { + return 0; + } + + GameTeam team = game.getTeam(color); + if (team == null) { + return 0; + } + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private static Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) { + return null; + } + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) { + return game; + } + } + + Game game = room.getGame(com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame.class); + if (game != null) { + return game; + } + + game = room.getGame(com.eu.habbo.habbohotel.games.freeze.FreezeGame.class); + if (game != null) { + return game; + } + + return room.getGame(com.eu.habbo.habbohotel.games.wired.WiredGame.class); + } + + private static boolean hasRoomEntryMethod(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return false; + } + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + return roomEntryMethod != null && !roomEntryMethod.trim().isEmpty() && !"unknown".equalsIgnoreCase(roomEntryMethod); + } + + private static Integer getRoomEntryMethodValue(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return null; + } + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + + if (roomEntryMethod == null || roomEntryMethod.trim().isEmpty()) { + return 0; + } + + return switch (roomEntryMethod.trim().toLowerCase(Locale.ROOT)) { + case "door" -> 1; + case "teleport" -> 2; + default -> 3; + }; + } + + private static int parseStatusInteger(RoomUnit roomUnit, RoomUnitStatus status) { + if (roomUnit == null || status == null || !roomUnit.hasStatus(status)) { + return 0; + } + + return parseInteger(roomUnit.getStatus(status)); + } + + private static boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y) { + if (room == null || roomUnit == null || room.getLayout() == null) { + return false; + } + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) { + return false; + } + + double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); + return WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), 0, true); + } + + private static boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { + if (room == null || item == null || room.getLayout() == null) { + return false; + } + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) { + return false; + } + + FurnitureMovementError error = room.moveFurniTo(item, targetTile, rotation, z, null, true, true); + return error == FurnitureMovementError.NONE; + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException exception) { + return 0; + } + } + + private static int countIterable(Iterable values) { + if (values == null) { + return 0; + } + + int count = 0; + + for (Object ignored : values) { + count++; + } + + return count; + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java index a3077406..82456ac9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java @@ -40,29 +40,21 @@ import java.sql.ResultSet; import java.sql.SQLException; /** - * Manager class for the new wired engine system. + * Manager class for the wired runtime. *

- * This class serves as the integration point between the emulator and the new - * wired engine. It provides static methods for triggering events and manages - * the lifecycle of the engine. + * WiredManager is now the sole runtime entrypoint for wired execution. Legacy + * configuration keys are still read for backwards compatibility with existing + * databases, but they no longer switch execution back to {@code WiredHandler}. *

- * + * *

Configuration Options:

*
    - *
  • {@code wired.engine.enabled} - Enable new engine (parallel mode)
  • - *
  • {@code wired.engine.exclusive} - Disable legacy handler when true
  • + *
  • {@code wired.engine.enabled} - Compatibility flag kept for old configs
  • + *
  • {@code wired.engine.exclusive} - Compatibility flag kept for old configs
  • *
  • {@code wired.engine.maxStepsPerStack} - Loop protection limit
  • *
  • {@code wired.engine.debug} - Verbose logging
  • *
- * - *

Migration Strategy:

- *
    - *
  1. Set {@code wired.engine.enabled=true} to run both engines in parallel
  2. - *
  3. Test thoroughly to ensure identical behavior
  4. - *
  5. Set {@code wired.engine.exclusive=true} to disable legacy engine
  6. - *
  7. Full migration complete - WiredManager is now the only wired engine
  8. - *
- * + * * @see WiredEngine * @see WiredEvents */ @@ -80,8 +72,8 @@ public final class WiredManager { public static final String CONFIG_DEBUG = "wired.engine.debug"; // Defaults - private static final boolean DEFAULT_ENABLED = false; - private static final boolean DEFAULT_EXCLUSIVE = false; + private static final boolean DEFAULT_ENABLED = true; + private static final boolean DEFAULT_EXCLUSIVE = true; private static final int DEFAULT_MAX_STEPS = 100; /** The singleton engine instance */ @@ -117,6 +109,7 @@ public final class WiredManager { // Load configuration boolean enabled = Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED); + boolean exclusive = Emulator.getConfig().getBoolean(CONFIG_EXCLUSIVE, DEFAULT_EXCLUSIVE); int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS); boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false); @@ -138,9 +131,13 @@ public final class WiredManager { WiredTickService.getInstance().start(); initialized = true; - - LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}", - enabled, maxSteps, debug); + + if (!enabled || !exclusive) { + 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); } /** @@ -163,6 +160,7 @@ public final class WiredManager { if (engine != null) { engine.clearUnseenCache(); + engine.clearAllDiagnostics(); } initialized = false; @@ -174,7 +172,7 @@ public final class WiredManager { * @return true if enabled */ public static boolean isEnabled() { - return Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED); + return initialized && engine != null; } /** @@ -182,7 +180,7 @@ public final class WiredManager { * @return true if exclusive mode */ public static boolean isExclusive() { - return Emulator.getConfig().getBoolean(CONFIG_EXCLUSIVE, DEFAULT_EXCLUSIVE); + return true; } /** @@ -201,6 +199,27 @@ public final class WiredManager { return stackIndex; } + /** + * Get the current monitor snapshot for a room. + * @param roomId the room ID + * @return the diagnostics snapshot, or null if the engine is unavailable + */ + public static WiredRoomDiagnostics.Snapshot getDiagnosticsSnapshot(int roomId) { + if (engine == null) { + return null; + } + + return engine.getDiagnosticsSnapshot(roomId); + } + + public static void clearDiagnosticsLogs(int roomId) { + if (engine == null) { + return; + } + + engine.clearRoomDiagnosticsLogs(roomId); + } + // ========== Event Triggering Methods ========== /** @@ -308,20 +327,28 @@ public final class WiredManager { * Trigger when a user says something. */ public static boolean triggerUserSays(Room room, RoomUnit user, String message) { + return triggerUserSays(room, user, message, -1, -1); + } + + public static boolean triggerUserSays(Room room, RoomUnit user, String message, int chatType, int chatStyle) { if (!isEnabled() || room == null || user == null) { return false; } - - WiredEvent event = WiredEvents.userSays(room, user, message); + + WiredEvent event = WiredEvents.userSays(room, user, message, chatType, chatStyle); return handleEvent(event); } public static boolean shouldSuppressUserSaysOutput(Room room, RoomUnit user, String message) { + return shouldSuppressUserSaysOutput(room, user, message, -1, -1); + } + + public static boolean shouldSuppressUserSaysOutput(Room room, RoomUnit user, String message, int chatType, int chatStyle) { if (!isEnabled() || engine == null || room == null || user == null) { return false; } - WiredEvent event = WiredEvents.userSays(room, user, message); + WiredEvent event = WiredEvents.userSays(room, user, message, chatType, chatStyle); return engine.shouldSuppressUserSaysOutput(event); } @@ -361,6 +388,36 @@ public final class WiredManager { return handleEvent(event); } + public static boolean triggerUserVariableChanged(Room room, int userId, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + if (!isEnabled() || room == null || definitionItemId <= 0) { + return false; + } + + Habbo habbo = room.getHabbo(userId); + RoomUnit roomUnit = (habbo != null) ? habbo.getRoomUnit() : null; + WiredEvent event = WiredEvents.userVariableChanged(room, roomUnit, definitionItemId, created, deleted, changeKind); + return handleEvent(event); + } + + public static boolean triggerFurniVariableChanged(Room room, int furniId, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + if (!isEnabled() || room == null || furniId <= 0 || definitionItemId <= 0) { + return false; + } + + HabboItem item = room.getHabboItem(furniId); + WiredEvent event = WiredEvents.furniVariableChanged(room, item, definitionItemId, created, deleted, changeKind); + return handleEvent(event); + } + + public static boolean triggerRoomVariableChanged(Room room, int definitionItemId, WiredEvent.VariableChangeKind changeKind) { + if (!isEnabled() || room == null || definitionItemId <= 0) { + return false; + } + + WiredEvent event = WiredEvents.roomVariableChanged(room, definitionItemId, changeKind); + return handleEvent(event); + } + /** * Trigger a timer tick. */ @@ -567,8 +624,8 @@ public final class WiredManager { } /** - * Trigger from legacy system for parallel running. - * This allows the new engine to run alongside the old one during migration. + * Compatibility bridge for code paths that still describe themselves as + * legacy-triggered. Execution still goes through the new engine only. */ public static boolean triggerFromLegacy(WiredTriggerType triggerType, RoomUnit roomUnit, Room room, Object[] stuff) { if (!isEnabled() || room == null) { @@ -725,6 +782,11 @@ public final class WiredManager { */ public static void unregisterRoomTickables(Room room) { WiredTickService.getInstance().unregisterRoom(room); + + if (room != null) { + room.getFurniVariableManager().clearTransientAssignments(); + room.getRoomVariableManager().clearTransientAssignments(); + } } /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java index 74ab5e11..49e14b04 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java @@ -176,7 +176,25 @@ public final class WiredMoveCarryHelper { && !sendUpdates && oldLocation != null && (oldLocation.x != targetTile.x || oldLocation.y != targetTile.y || Double.compare(oldZ, movingItem.getZ()) != 0)) { - room.sendComposer(new FloorItemOnRollerComposer(movingItem, null, oldLocation, oldZ, targetTile, movingItem.getZ(), 0, room).compose()); + List collectedMovements = COLLECTED_MOVEMENTS.get(); + + if (collectedMovements != null) { + collectedMovements.add(WiredMovementsComposer.furniMovement( + movingItem.getId(), + oldLocation.x, + oldLocation.y, + targetTile.x, + targetTile.y, + oldZ, + movingItem.getZ(), + movingItem.getRotation(), + WiredMovementsComposer.DEFAULT_DURATION, + 0, + WiredMovementsComposer.FURNI_ANCHOR_NONE, + 0)); + } else { + room.sendComposer(new FloorItemOnRollerComposer(movingItem, null, oldLocation, oldZ, targetTile, movingItem.getZ(), 0, room).compose()); + } } return result; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java new file mode 100644 index 00000000..caa7e349 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java @@ -0,0 +1,586 @@ +package com.eu.habbo.habbohotel.wired.core; + +import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; + +/** + * Tracks wired monitor data for a single room. + */ +public final class WiredRoomDiagnostics { + + public enum Type { + EXECUTION_CAP, + DELAYED_EVENTS_CAP, + EXECUTOR_OVERLOAD, + MARKED_AS_HEAVY, + KILLED, + RECURSION_TIMEOUT + } + + public enum Severity { + WARNING, + ERROR + } + + public static final class LogEntry { + private final Type type; + private final Severity severity; + private int count; + private long firstOccurredAtMs; + private long lastOccurredAtMs; + private String latestReason; + private String latestSourceLabel; + private int latestSourceId; + + private LogEntry(Type type, Severity severity) { + this.type = type; + this.severity = severity; + } + + private void record(long now, String reason, String sourceLabel, int sourceId) { + if (this.count <= 0) { + this.firstOccurredAtMs = now; + } + + this.count++; + this.lastOccurredAtMs = now; + this.latestReason = sanitizeReason(reason); + this.latestSourceLabel = sanitizeSourceLabel(sourceLabel); + this.latestSourceId = Math.max(0, sourceId); + } + + public Type getType() { + return type; + } + + public Severity getSeverity() { + return severity; + } + + public int getCount() { + return count; + } + + public long getFirstOccurredAtMs() { + return firstOccurredAtMs; + } + + public long getLastOccurredAtMs() { + return lastOccurredAtMs; + } + + public String getLatestReason() { + return latestReason; + } + + public String getLatestSourceLabel() { + return latestSourceLabel; + } + + public int getLatestSourceId() { + return latestSourceId; + } + } + + public static final class Snapshot { + private final int usageCurrentWindow; + private final int usageLimitPerWindow; + private final boolean heavy; + private final int delayedEventsPending; + private final int delayedEventsLimit; + private final int averageExecutionMs; + private final int peakExecutionMs; + private final int recursionDepthCurrent; + private final int recursionDepthLimit; + private final int killedRemainingSeconds; + private final int usageWindowMs; + private final int overloadAverageThresholdMs; + private final int overloadPeakThresholdMs; + private final int heavyUsageThresholdPercent; + private final int heavyConsecutiveWindowsThreshold; + private final int overloadConsecutiveWindowsThreshold; + private final int heavyDelayedThresholdPercent; + private final List logs; + private final List history; + + public Snapshot(int usageCurrentWindow, int usageLimitPerWindow, boolean heavy, int delayedEventsPending, + int delayedEventsLimit, int averageExecutionMs, int peakExecutionMs, + int recursionDepthCurrent, int recursionDepthLimit, int killedRemainingSeconds, + int usageWindowMs, int overloadAverageThresholdMs, int overloadPeakThresholdMs, + int heavyUsageThresholdPercent, int heavyConsecutiveWindowsThreshold, + int overloadConsecutiveWindowsThreshold, int heavyDelayedThresholdPercent, + List logs, List history) { + this.usageCurrentWindow = usageCurrentWindow; + this.usageLimitPerWindow = usageLimitPerWindow; + this.heavy = heavy; + this.delayedEventsPending = delayedEventsPending; + this.delayedEventsLimit = delayedEventsLimit; + this.averageExecutionMs = averageExecutionMs; + this.peakExecutionMs = peakExecutionMs; + this.recursionDepthCurrent = recursionDepthCurrent; + this.recursionDepthLimit = recursionDepthLimit; + this.killedRemainingSeconds = killedRemainingSeconds; + this.usageWindowMs = usageWindowMs; + this.overloadAverageThresholdMs = overloadAverageThresholdMs; + this.overloadPeakThresholdMs = overloadPeakThresholdMs; + this.heavyUsageThresholdPercent = heavyUsageThresholdPercent; + this.heavyConsecutiveWindowsThreshold = heavyConsecutiveWindowsThreshold; + this.overloadConsecutiveWindowsThreshold = overloadConsecutiveWindowsThreshold; + this.heavyDelayedThresholdPercent = heavyDelayedThresholdPercent; + this.logs = Collections.unmodifiableList(logs); + this.history = Collections.unmodifiableList(history); + } + + public int getUsageCurrentWindow() { + return usageCurrentWindow; + } + + public int getUsageLimitPerWindow() { + return usageLimitPerWindow; + } + + public boolean isHeavy() { + return heavy; + } + + public int getDelayedEventsPending() { + return delayedEventsPending; + } + + public int getDelayedEventsLimit() { + return delayedEventsLimit; + } + + public int getAverageExecutionMs() { + return averageExecutionMs; + } + + public int getPeakExecutionMs() { + return peakExecutionMs; + } + + public int getRecursionDepthCurrent() { + return recursionDepthCurrent; + } + + public int getRecursionDepthLimit() { + return recursionDepthLimit; + } + + public int getKilledRemainingSeconds() { + return killedRemainingSeconds; + } + + public int getUsageWindowMs() { + return usageWindowMs; + } + + public int getOverloadAverageThresholdMs() { + return overloadAverageThresholdMs; + } + + public int getOverloadPeakThresholdMs() { + return overloadPeakThresholdMs; + } + + public int getHeavyUsageThresholdPercent() { + return heavyUsageThresholdPercent; + } + + public int getHeavyConsecutiveWindowsThreshold() { + return heavyConsecutiveWindowsThreshold; + } + + public int getOverloadConsecutiveWindowsThreshold() { + return overloadConsecutiveWindowsThreshold; + } + + public int getHeavyDelayedThresholdPercent() { + return heavyDelayedThresholdPercent; + } + + public List getLogs() { + return logs; + } + + public List getHistory() { + return history; + } + } + + public static final class HistoryEntry { + private final Type type; + private final Severity severity; + private final long occurredAtMs; + private final String reason; + private final String sourceLabel; + private final int sourceId; + + public HistoryEntry(Type type, Severity severity, long occurredAtMs, String reason, String sourceLabel, int sourceId) { + this.type = type; + this.severity = severity; + this.occurredAtMs = occurredAtMs; + this.reason = sanitizeReason(reason); + this.sourceLabel = sanitizeSourceLabel(sourceLabel); + this.sourceId = Math.max(0, sourceId); + } + + public Type getType() { + return type; + } + + public Severity getSeverity() { + return severity; + } + + public long getOccurredAtMs() { + return occurredAtMs; + } + + public String getReason() { + return reason; + } + + public String getSourceLabel() { + return sourceLabel; + } + + public int getSourceId() { + return sourceId; + } + } + + private final int usageWindowMs; + private final int usageLimitPerWindow; + private final int delayedEventsLimit; + private final int overloadAverageThresholdMs; + private final int overloadPeakThresholdMs; + private final int heavyUsageThresholdPercent; + private final int heavyConsecutiveWindowsThreshold; + private final int overloadConsecutiveWindowsThreshold; + private final int heavyDelayedThresholdPercent; + private final EnumMap logs; + private final ArrayDeque history; + private final int maxHistoryEntries; + + private long windowStartedAt; + private int usageCurrentWindow; + private int delayedEventsPending; + private long totalExecutionMsCurrentWindow; + private int executionSamplesCurrentWindow; + private int averageExecutionMs; + private int peakExecutionMs; + private int consecutiveHeavyWindows; + private int consecutiveOverloadWindows; + private boolean heavy; + private String peakExecutionSourceLabel; + private int peakExecutionSourceId; + private String peakExecutionReason; + + public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit, + int overloadAverageThresholdMs, int overloadPeakThresholdMs, + int heavyUsageThresholdPercent, int heavyConsecutiveWindowsThreshold) { + this(usageWindowMs, usageLimitPerWindow, delayedEventsLimit, overloadAverageThresholdMs, overloadPeakThresholdMs, + heavyUsageThresholdPercent, heavyConsecutiveWindowsThreshold, 2, 60, 200); + } + + public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit, + int overloadAverageThresholdMs, int overloadPeakThresholdMs, + int heavyUsageThresholdPercent, int heavyConsecutiveWindowsThreshold, + int overloadConsecutiveWindowsThreshold, int heavyDelayedThresholdPercent, + int maxHistoryEntries) { + this.usageWindowMs = Math.max(250, usageWindowMs); + this.usageLimitPerWindow = Math.max(1, usageLimitPerWindow); + this.delayedEventsLimit = Math.max(1, delayedEventsLimit); + this.overloadAverageThresholdMs = Math.max(1, overloadAverageThresholdMs); + this.overloadPeakThresholdMs = Math.max(this.overloadAverageThresholdMs, overloadPeakThresholdMs); + this.heavyUsageThresholdPercent = Math.max(1, Math.min(100, heavyUsageThresholdPercent)); + this.heavyConsecutiveWindowsThreshold = Math.max(1, heavyConsecutiveWindowsThreshold); + this.overloadConsecutiveWindowsThreshold = Math.max(1, overloadConsecutiveWindowsThreshold); + this.heavyDelayedThresholdPercent = Math.max(1, Math.min(100, heavyDelayedThresholdPercent)); + this.maxHistoryEntries = Math.max(10, maxHistoryEntries); + this.logs = new EnumMap<>(Type.class); + this.history = new ArrayDeque<>(this.maxHistoryEntries); + + for (Type type : Type.values()) { + this.logs.put(type, new LogEntry(type, defaultSeverity(type))); + } + } + + public synchronized boolean tryConsumeExecutionBudget(int estimatedCost, long now, String sourceLabel, int sourceId, String reason) { + rollWindow(now); + + int normalizedCost = Math.max(0, estimatedCost); + if ((this.usageCurrentWindow + normalizedCost) > this.usageLimitPerWindow) { + record(Type.EXECUTION_CAP, now, + buildExecutionCapReason(normalizedCost, reason), + sourceLabel, + sourceId); + return false; + } + + this.usageCurrentWindow += normalizedCost; + return true; + } + + public synchronized boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) { + rollWindow(now); + + if ((this.delayedEventsPending + 1) > this.delayedEventsLimit) { + record(Type.DELAYED_EVENTS_CAP, now, + buildDelayedCapReason(reason), + sourceLabel, + sourceId); + return false; + } + + this.delayedEventsPending++; + return true; + } + + public synchronized void completeDelayedEvent() { + if (this.delayedEventsPending > 0) { + this.delayedEventsPending--; + } + } + + public synchronized void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) { + rollWindow(now); + + int normalizedElapsed = (int) Math.max(0L, elapsedMs); + + this.totalExecutionMsCurrentWindow += normalizedElapsed; + this.executionSamplesCurrentWindow++; + this.averageExecutionMs = (int) Math.round(this.totalExecutionMsCurrentWindow / (double) this.executionSamplesCurrentWindow); + + if (normalizedElapsed >= this.peakExecutionMs) { + this.peakExecutionMs = normalizedElapsed; + this.peakExecutionSourceLabel = sanitizeSourceLabel(sourceLabel); + this.peakExecutionSourceId = Math.max(0, sourceId); + this.peakExecutionReason = sanitizeReason(reason); + } + } + + public synchronized void recordKilled(long now, String reason, String sourceLabel, int sourceId) { + rollWindow(now); + record(Type.KILLED, now, reason, sourceLabel, sourceId); + } + + public synchronized void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) { + rollWindow(now); + record(Type.RECURSION_TIMEOUT, now, reason, sourceLabel, sourceId); + } + + public synchronized void clearLogs() { + for (Type type : Type.values()) { + LogEntry entry = this.logs.get(type); + + if (entry == null) { + continue; + } + + entry.count = 0; + entry.firstOccurredAtMs = 0L; + entry.lastOccurredAtMs = 0L; + entry.latestReason = ""; + entry.latestSourceLabel = ""; + entry.latestSourceId = 0; + } + + this.history.clear(); + } + + public synchronized Snapshot snapshot(int recursionDepthCurrent, int recursionDepthLimit, long killedUntilMs, long now) { + rollWindow(now); + + List logEntries = new ArrayList<>(Type.values().length); + List historyEntries = new ArrayList<>(this.history.size()); + + for (Type type : Type.values()) { + LogEntry source = this.logs.get(type); + LogEntry copy = new LogEntry(source.getType(), source.getSeverity()); + + copy.count = source.getCount(); + copy.firstOccurredAtMs = source.getFirstOccurredAtMs(); + copy.lastOccurredAtMs = source.getLastOccurredAtMs(); + copy.latestReason = source.getLatestReason(); + copy.latestSourceLabel = source.getLatestSourceLabel(); + copy.latestSourceId = source.getLatestSourceId(); + + logEntries.add(copy); + } + + historyEntries.addAll(this.history); + + int killedRemainingSeconds = 0; + + if (killedUntilMs > now) { + killedRemainingSeconds = (int) Math.max(0L, Math.ceil((killedUntilMs - now) / 1000D)); + } + + return new Snapshot( + this.usageCurrentWindow, + this.usageLimitPerWindow, + this.heavy, + this.delayedEventsPending, + this.delayedEventsLimit, + this.averageExecutionMs, + this.peakExecutionMs, + recursionDepthCurrent, + recursionDepthLimit, + killedRemainingSeconds, + this.usageWindowMs, + this.overloadAverageThresholdMs, + this.overloadPeakThresholdMs, + this.heavyUsageThresholdPercent, + this.heavyConsecutiveWindowsThreshold, + this.overloadConsecutiveWindowsThreshold, + this.heavyDelayedThresholdPercent, + logEntries, + historyEntries + ); + } + + private void rollWindow(long now) { + if (this.windowStartedAt <= 0L) { + this.windowStartedAt = now; + return; + } + + while ((now - this.windowStartedAt) >= this.usageWindowMs) { + evaluateWindow(this.windowStartedAt + this.usageWindowMs); + this.windowStartedAt += this.usageWindowMs; + this.usageCurrentWindow = 0; + this.totalExecutionMsCurrentWindow = 0L; + this.executionSamplesCurrentWindow = 0; + this.averageExecutionMs = 0; + this.peakExecutionMs = 0; + this.peakExecutionSourceLabel = null; + this.peakExecutionSourceId = 0; + this.peakExecutionReason = null; + } + } + + private void evaluateWindow(long now) { + int usagePercent = (int) Math.round((this.usageCurrentWindow * 100D) / this.usageLimitPerWindow); + int delayedPercent = (int) Math.round((this.delayedEventsPending * 100D) / this.delayedEventsLimit); + boolean overloadWindow = (this.executionSamplesCurrentWindow > 0) + && ((this.averageExecutionMs >= this.overloadAverageThresholdMs) || (this.peakExecutionMs >= this.overloadPeakThresholdMs)); + boolean heavyWindow = (usagePercent >= this.heavyUsageThresholdPercent) + || (delayedPercent >= this.heavyDelayedThresholdPercent) + || overloadWindow; + + if (overloadWindow) { + this.consecutiveOverloadWindows++; + + if (this.consecutiveOverloadWindows >= this.overloadConsecutiveWindowsThreshold) { + record(Type.EXECUTOR_OVERLOAD, now, + buildExecutorOverloadReason(), + this.peakExecutionSourceLabel, + this.peakExecutionSourceId); + } + } else { + this.consecutiveOverloadWindows = 0; + } + + if (heavyWindow) { + this.consecutiveHeavyWindows++; + + if (!this.heavy && (this.consecutiveHeavyWindows >= this.heavyConsecutiveWindowsThreshold)) { + this.heavy = true; + record(Type.MARKED_AS_HEAVY, now, + buildHeavyReason(usagePercent, delayedPercent, overloadWindow), + overloadWindow ? this.peakExecutionSourceLabel : null, + overloadWindow ? this.peakExecutionSourceId : 0); + } + + return; + } + + this.consecutiveHeavyWindows = 0; + this.heavy = false; + } + + private void record(Type type, long now, String reason, String sourceLabel, int sourceId) { + LogEntry entry = this.logs.get(type); + if (entry != null) { + entry.record(now, reason, sourceLabel, sourceId); + this.history.addFirst(new HistoryEntry(type, entry.getSeverity(), now, reason, sourceLabel, sourceId)); + + while (this.history.size() > this.maxHistoryEntries) { + this.history.removeLast(); + } + } + } + + private String buildExecutionCapReason(int normalizedCost, String reason) { + return joinReason( + reason, + String.format("Estimated stack cost %d would exceed usage budget %d/%d in %dms window", + normalizedCost, + this.usageCurrentWindow, + this.usageLimitPerWindow, + this.usageWindowMs) + ); + } + + private String buildDelayedCapReason(String reason) { + return joinReason( + reason, + String.format("Pending delayed events would exceed queue %d/%d", + this.delayedEventsPending, + this.delayedEventsLimit) + ); + } + + private String buildExecutorOverloadReason() { + return joinReason( + this.peakExecutionReason, + String.format("Average execution %dms (limit %dms), peak %dms (limit %dms) across %d execution(s) in %dms window", + this.averageExecutionMs, + this.overloadAverageThresholdMs, + this.peakExecutionMs, + this.overloadPeakThresholdMs, + this.executionSamplesCurrentWindow, + this.usageWindowMs) + ); + } + + private String buildHeavyReason(int usagePercent, int delayedPercent, boolean overloadWindow) { + return String.format( + "Room stayed above heavy thresholds for %d consecutive window(s): usage %d%%/%d%%, delayed %d%%/%d%%, overload %s", + this.consecutiveHeavyWindows, + usagePercent, + this.heavyUsageThresholdPercent, + delayedPercent, + this.heavyDelayedThresholdPercent, + overloadWindow ? "yes" : "no" + ); + } + + private static String joinReason(String primary, String fallback) { + String cleanPrimary = sanitizeReason(primary); + String cleanFallback = sanitizeReason(fallback); + + if (cleanPrimary.isEmpty()) return cleanFallback; + if (cleanFallback.isEmpty()) return cleanPrimary; + if (cleanPrimary.equals(cleanFallback)) return cleanPrimary; + + return cleanPrimary + ". " + cleanFallback; + } + + private static String sanitizeReason(String reason) { + return (reason == null) ? "" : reason.trim(); + } + + private static String sanitizeSourceLabel(String sourceLabel) { + return (sourceLabel == null) ? "" : sourceLabel.trim(); + } + + private Severity defaultSeverity(Type type) { + return (type == Type.MARKED_AS_HEAVY) ? Severity.WARNING : Severity.ERROR; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java index 8e4dbd75..a400076c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java @@ -4,7 +4,9 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; @@ -233,21 +235,50 @@ public final class WiredSourceUtil { int furniLimit = Integer.MAX_VALUE; int userLimit = Integer.MAX_VALUE; + List furniVariableFilters = new ArrayList<>(); + List userVariableFilters = new ArrayList<>(); for (InteractionWiredExtra extra : extras) { if (extra instanceof WiredExtraFilterFurni) { furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); } else if (extra instanceof WiredExtraFilterUser) { userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); + } else if (extra instanceof WiredExtraFilterFurniByVariable) { + furniVariableFilters.add((WiredExtraFilterFurniByVariable) extra); + } else if (extra instanceof WiredExtraFilterUsersByVariable) { + userVariableFilters.add((WiredExtraFilterUsersByVariable) extra); } } - if (selectorCtx.targets().isItemsModifiedBySelector() && furniLimit != Integer.MAX_VALUE) { - selectorCtx.targets().setItems(limitIterable(selectorCtx.targets().items(), furniLimit)); + furniVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + userVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + + if (selectorCtx.targets().isItemsModifiedBySelector()) { + Iterable filteredItems = selectorCtx.targets().items(); + + for (WiredExtraFilterFurniByVariable extra : furniVariableFilters) { + filteredItems = extra.filterItems(room, selectorCtx, filteredItems); + } + + if (furniLimit != Integer.MAX_VALUE) { + filteredItems = limitIterable(filteredItems, furniLimit); + } + + selectorCtx.targets().setItems(filteredItems); } - if (selectorCtx.targets().isUsersModifiedBySelector() && userLimit != Integer.MAX_VALUE) { - selectorCtx.targets().setUsers(limitIterable(selectorCtx.targets().users(), userLimit)); + if (selectorCtx.targets().isUsersModifiedBySelector()) { + Iterable filteredUsers = selectorCtx.targets().users(); + + for (WiredExtraFilterUsersByVariable extra : userVariableFilters) { + filteredUsers = extra.filterUsers(room, selectorCtx, filteredUsers); + } + + if (userLimit != Integer.MAX_VALUE) { + filteredUsers = limitIterable(filteredUsers, userLimit); + } + + selectorCtx.targets().setUsers(filteredUsers); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java new file mode 100644 index 00000000..cb73d427 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java @@ -0,0 +1,246 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextInputVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboSaysKeyword; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.api.WiredStack; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class WiredTextInputCaptureSupport { + private static final int MATCH_CONTAINS = 0; + private static final int MATCH_EXACT = 1; + private static final int MATCH_ALL_WORDS = 2; + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("#([^#]+)#"); + + private WiredTextInputCaptureSupport() { + } + + public static CaptureResult resolve(WiredStack stack, WiredEvent event) { + if (stack == null || event == null || !(stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword)) { + return CaptureResult.noMatch(); + } + + WiredTriggerHabboSaysKeyword trigger = (WiredTriggerHabboSaysKeyword) stack.triggerItem(); + Room room = event.getRoom(); + RoomUnit actor = event.getActor().orElse(null); + String text = event.getText().orElse(null); + + if (room == null || actor == null || text == null) { + return CaptureResult.noMatch(); + } + + List capturers = getCapturers(room, trigger); + if (capturers.isEmpty()) { + return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch(); + } + + if (trigger.isOwnerOnly()) { + Habbo habbo = room.getHabbo(actor); + if (habbo == null || room.getOwnerId() != habbo.getHabboInfo().getId()) { + return CaptureResult.noMatch(); + } + } + + LinkedHashMap capturersByName = new LinkedHashMap<>(); + for (WiredExtraTextInputVariable capturer : capturers) { + if (capturer == null || capturer.getCapturerName() == null || capturer.getCapturerName().trim().isEmpty()) { + continue; + } + + capturersByName.put(capturer.getCapturerName().trim().toLowerCase(), capturer); + } + + if (capturersByName.isEmpty()) { + return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch(); + } + + MatchResult matchResult = matchTemplate(trigger, text, capturersByName); + if (!matchResult.matches) { + return CaptureResult.noMatch(); + } + + LinkedHashMap capturedValues = new LinkedHashMap<>(); + for (Map.Entry capture : matchResult.captures.entrySet()) { + WiredExtraTextInputVariable capturer = capturersByName.get(capture.getKey()); + if (capturer == null) { + continue; + } + + Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue()); + if (resolvedValue == null) { + return CaptureResult.noMatch(); + } + + capturedValues.put(capturer.getVariableItemId(), resolvedValue); + } + + return CaptureResult.matched(capturedValues); + } + + private static List getCapturers(Room room, WiredTriggerHabboSaysKeyword trigger) { + List capturers = new ArrayList<>(); + + if (room == null || trigger == null || room.getRoomSpecialTypes() == null) { + return capturers; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY()); + if (extras == null || extras.isEmpty()) { + return capturers; + } + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (extra instanceof WiredExtraTextInputVariable) { + capturers.add((WiredExtraTextInputVariable) extra); + } + } + + return capturers; + } + + private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map capturersByName) { + String text = rawText != null ? rawText.trim() : ""; + String template = trigger.getKey() != null ? trigger.getKey().trim() : ""; + + if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) { + if (capturersByName.size() != 1 || text.isEmpty()) { + return MatchResult.noMatch(); + } + + String placeholderName = capturersByName.keySet().iterator().next(); + LinkedHashMap captures = new LinkedHashMap<>(); + captures.put(placeholderName, text); + return MatchResult.matched(captures); + } + + TemplatePattern pattern = buildPattern(template); + if (pattern == null) { + return MatchResult.noMatch(); + } + + Matcher matcher = pattern.pattern.matcher(text); + boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches(); + if (!matches) { + return MatchResult.noMatch(); + } + + LinkedHashMap captures = new LinkedHashMap<>(); + for (int index = 0; index < pattern.placeholderNames.size(); index++) { + String placeholderName = pattern.placeholderNames.get(index); + if (!capturersByName.containsKey(placeholderName)) { + continue; + } + + String capturedValue = matcher.group(index + 1); + captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : ""); + } + + return MatchResult.matched(captures); + } + + private static TemplatePattern buildPattern(String template) { + if (template == null || template.isEmpty()) { + return null; + } + + Matcher matcher = PLACEHOLDER_PATTERN.matcher(template); + StringBuilder regex = new StringBuilder(); + List placeholderNames = new ArrayList<>(); + int cursor = 0; + + while (matcher.find()) { + regex.append(Pattern.quote(template.substring(cursor, matcher.start()))); + regex.append("(.+?)"); + + String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : ""; + placeholderNames.add(placeholderName); + cursor = matcher.end(); + } + + regex.append(Pattern.quote(template.substring(cursor))); + + if (placeholderNames.isEmpty()) { + regex = new StringBuilder(Pattern.quote(template)); + } + + return new TemplatePattern(Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE), placeholderNames); + } + + public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) { + if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) { + return; + } + + ctx.forkContextVariables(); + + for (Map.Entry entry : captureResult.capturedValues.entrySet()) { + if (entry == null || entry.getKey() == null || entry.getKey() <= 0) { + continue; + } + + if (!WiredContextVariableSupport.updateVariableValue(ctx, room, entry.getKey(), entry.getValue())) { + WiredContextVariableSupport.assignVariable(ctx, room, entry.getKey(), entry.getValue(), false); + } + } + } + + public static final class CaptureResult { + private final boolean matches; + private final LinkedHashMap capturedValues; + + private CaptureResult(boolean matches, LinkedHashMap capturedValues) { + this.matches = matches; + this.capturedValues = capturedValues; + } + + public boolean matches() { + return this.matches; + } + + public static CaptureResult matched(LinkedHashMap capturedValues) { + return new CaptureResult(true, capturedValues); + } + + public static CaptureResult noMatch() { + return new CaptureResult(false, new LinkedHashMap<>()); + } + } + + private static final class MatchResult { + private final boolean matches; + private final LinkedHashMap captures; + + private MatchResult(boolean matches, LinkedHashMap captures) { + this.matches = matches; + this.captures = captures; + } + + private static MatchResult matched(LinkedHashMap captures) { + return new MatchResult(true, captures); + } + + private static MatchResult noMatch() { + return new MatchResult(false, new LinkedHashMap<>()); + } + } + + private static final class TemplatePattern { + private final Pattern pattern; + private final List placeholderNames; + + private TemplatePattern(Pattern pattern, List placeholderNames) { + this.pattern = pattern; + this.placeholderNames = placeholderNames; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java index d9b86538..d1db42ab 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java @@ -1,20 +1,35 @@ package com.eu.habbo.habbohotel.wired.core; import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputFurniName; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputUsername; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; import com.eu.habbo.habbohotel.pets.Pet; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnitType; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.util.HotelDateTimeUtil; import gnu.trove.set.hash.THashSet; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; public final class WiredTextPlaceholderUtil { private WiredTextPlaceholderUtil() { @@ -58,6 +73,17 @@ public final class WiredTextPlaceholderUtil { if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { resolvedText = resolvedText.replace(placeholderToken, buildFurniNameReplacement(ctx, furniExtra)); } + + continue; + } + + if (extra instanceof WiredExtraTextOutputVariable) { + WiredExtraTextOutputVariable variableExtra = (WiredExtraTextOutputVariable) extra; + String placeholderToken = variableExtra.getPlaceholderToken(); + + if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { + resolvedText = resolvedText.replace(placeholderToken, buildVariableReplacement(ctx, variableExtra)); + } } } @@ -75,12 +101,14 @@ public final class WiredTextPlaceholderUtil { } for (InteractionWiredExtra extra : extras) { - if (!(extra instanceof WiredExtraTextOutputUsername)) { - continue; + if (extra instanceof WiredExtraTextOutputUsername) { + int userSource = ((WiredExtraTextOutputUsername) extra).getUserSource(); + if (userSource == WiredSourceUtil.SOURCE_TRIGGER || userSource == WiredSourceUtil.SOURCE_CLICKED_USER) { + return true; + } } - int userSource = ((WiredExtraTextOutputUsername) extra).getUserSource(); - if ((userSource == WiredSourceUtil.SOURCE_TRIGGER) || (userSource == WiredSourceUtil.SOURCE_CLICKED_USER)) { + if (extra instanceof WiredExtraTextOutputVariable && ((WiredExtraTextOutputVariable) extra).requiresActor()) { return true; } } @@ -164,6 +192,186 @@ public final class WiredTextPlaceholderUtil { return furniNames.get(0); } + private static String buildVariableReplacement(WiredContext ctx, WiredExtraTextOutputVariable extra) { + List values = switch (extra.getTargetType()) { + case WiredExtraTextOutputVariable.TARGET_FURNI -> collectFurniVariableValues(ctx, extra); + case WiredExtraTextOutputVariable.TARGET_CONTEXT -> collectContextVariableValues(ctx, extra); + case WiredExtraTextOutputVariable.TARGET_ROOM -> collectRoomVariableValues(ctx, extra); + default -> collectUserVariableValues(ctx, extra); + }; + + if (values.isEmpty()) { + return ""; + } + + if (extra.getPlaceholderType() == WiredExtraTextOutputVariable.TYPE_MULTIPLE) { + return String.join(extra.getDelimiter(), values); + } + + return values.get(0); + } + + private static List collectUserVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + Room room = ctx.room(); + List users = WiredSourceUtil.resolveUsers(ctx, extra.getUserSource()); + if (room == null || users.isEmpty()) { + return List.of(); + } + + LinkedHashSet seenUserIds = new LinkedHashSet<>(); + List values = new ArrayList<>(); + + for (RoomUnit roomUnit : users) { + if (roomUnit == null || !seenUserIds.add(roomUnit.getId())) { + continue; + } + + String value = resolveUserVariableValue(room, roomUnit, extra); + if (value != null && !value.isEmpty()) { + values.add(value); + } + } + + return values; + } + + private static List collectFurniVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + Room room = ctx.room(); + if (room == null) { + return List.of(); + } + + extra.refresh(room); + + List items = (extra.getFurniSource() == WiredSourceUtil.SOURCE_SELECTOR) + ? WiredSourceUtil.resolveSelectorItems(ctx, true) + : WiredSourceUtil.resolveItems(ctx, extra.getFurniSource(), extra.getItems()); + + if (items.isEmpty()) { + return List.of(); + } + + LinkedHashSet seenItemIds = new LinkedHashSet<>(); + List values = new ArrayList<>(); + + for (HabboItem item : items) { + if (item == null || !seenItemIds.add(item.getId())) { + continue; + } + + String value = resolveFurniVariableValue(room, item, extra); + if (value != null && !value.isEmpty()) { + values.add(value); + } + } + + return values; + } + + private static List collectRoomVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + Room room = ctx.room(); + if (room == null) { + return List.of(); + } + + String value = resolveRoomVariableValue(room, extra); + return (value == null || value.isEmpty()) ? List.of() : List.of(value); + } + + private static List collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + if (ctx == null) { + return List.of(); + } + + String value = resolveContextVariableValue(ctx, extra); + return (value == null || value.isEmpty()) ? List.of() : List.of(value); + } + + private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) { + if (room == null || roomUnit == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = readUserInternalValue(room, roomUnit, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null || !room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), extra.getVariableItemId())) { + return null; + } + + Integer value = room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), extra.getVariableItemId()); + if (extra.getDisplayType(room) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(room, extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + + private static String resolveFurniVariableValue(Room room, HabboItem item, WiredExtraTextOutputVariable extra) { + if (room == null || item == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = readFurniInternalValue(room, item, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + if (!room.getFurniVariableManager().hasVariable(item.getId(), extra.getVariableItemId())) { + return null; + } + + Integer value = room.getFurniVariableManager().getCurrentValue(item.getId(), extra.getVariableItemId()); + if (extra.getDisplayType(room) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(room, extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + + private static String resolveRoomVariableValue(Room room, WiredExtraTextOutputVariable extra) { + if (room == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = readRoomInternalValue(room, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + Integer value = room.getRoomVariableManager().getCurrentValue(extra.getVariableItemId()); + if (extra.getDisplayType(room) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(room, extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + + private static String resolveContextVariableValue(WiredContext ctx, WiredExtraTextOutputVariable extra) { + if (ctx == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = WiredInternalVariableSupport.readContextValue(ctx, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + if (!WiredContextVariableSupport.hasVariable(ctx, extra.getVariableItemId())) { + return null; + } + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, extra.getVariableItemId()); + if (extra.getDisplayType(ctx.room()) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(ctx.room(), extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + private static String getRoomUnitName(Room room, RoomUnit roomUnit) { if (room == null || roomUnit == null) { return ""; @@ -186,4 +394,123 @@ public final class WiredTextPlaceholderUtil { return ""; } + + private static Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private static Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private static Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private static Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) { + return null; + } + + Game game = resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) { + return gamePlayer.getScore(); + } + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private static Integer getTeamColorId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private static Integer getTeamTypeId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private static int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = resolveTeamGame(room, null); + if (game == null || color == null) { + return 0; + } + + GameTeam team = game.getTeam(color); + if (team == null) { + return 0; + } + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private static Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) { + return null; + } + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) { + return game; + } + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) { + return wiredGame; + } + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) { + return freezeGame; + } + + return room.getGame(BattleBanzaiGame.class); + } + + private static TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) { + return null; + } + + if (effectValue >= 223 && effectValue <= 226) { + return new TeamEffectData(effectValue - 222, 0); + } + + if (effectValue >= 33 && effectValue <= 36) { + return new TeamEffectData(effectValue - 32, 1); + } + + if (effectValue >= 40 && effectValue <= 43) { + return new TeamEffectData(effectValue - 39, 2); + } + + return null; + } + + private static Integer parseInteger(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java index 17bad58f..4e7d3ecb 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java @@ -4,7 +4,9 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; @@ -172,21 +174,50 @@ public final class WiredTriggerSourceUtil { int furniLimit = Integer.MAX_VALUE; int userLimit = Integer.MAX_VALUE; + List furniVariableFilters = new ArrayList<>(); + List userVariableFilters = new ArrayList<>(); for (InteractionWiredExtra extra : extras) { if (extra instanceof WiredExtraFilterFurni) { furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); } else if (extra instanceof WiredExtraFilterUser) { userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); + } else if (extra instanceof WiredExtraFilterFurniByVariable) { + furniVariableFilters.add((WiredExtraFilterFurniByVariable) extra); + } else if (extra instanceof WiredExtraFilterUsersByVariable) { + userVariableFilters.add((WiredExtraFilterUsersByVariable) extra); } } - if (selectorCtx.targets().isItemsModifiedBySelector() && furniLimit != Integer.MAX_VALUE) { - selectorCtx.targets().setItems(limitIterable(selectorCtx.targets().items(), furniLimit)); + furniVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + userVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + + if (selectorCtx.targets().isItemsModifiedBySelector()) { + Iterable filteredItems = selectorCtx.targets().items(); + + for (WiredExtraFilterFurniByVariable extra : furniVariableFilters) { + filteredItems = extra.filterItems(room, selectorCtx, filteredItems); + } + + if (furniLimit != Integer.MAX_VALUE) { + filteredItems = limitIterable(filteredItems, furniLimit); + } + + selectorCtx.targets().setItems(filteredItems); } - if (selectorCtx.targets().isUsersModifiedBySelector() && userLimit != Integer.MAX_VALUE) { - selectorCtx.targets().setUsers(limitIterable(selectorCtx.targets().users(), userLimit)); + if (selectorCtx.targets().isUsersModifiedBySelector()) { + Iterable filteredUsers = selectorCtx.targets().users(); + + for (WiredExtraFilterUsersByVariable extra : userVariableFilters) { + filteredUsers = extra.filterUsers(room, selectorCtx, filteredUsers); + } + + if (userLimit != Integer.MAX_VALUE) { + filteredUsers = limitIterable(filteredUsers, userLimit); + } + + selectorCtx.targets().setUsers(filteredUsers); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableLevelSystemSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableLevelSystemSupport.java new file mode 100644 index 00000000..61de371f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableLevelSystemSupport.java @@ -0,0 +1,564 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableLevelUpSystem; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class WiredVariableLevelSystemSupport { + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_ROOM = 3; + + private static final int SYNTHETIC_USER_OFFSET = 700_000_000; + private static final int SYNTHETIC_FURNI_OFFSET = 800_000_000; + private static final int SYNTHETIC_ROOM_OFFSET = 900_000_000; + private static final int SYNTHETIC_STRIDE = 16; + + private WiredVariableLevelSystemSupport() { + } + + public static WiredExtraVariableLevelUpSystem getLevelSystem(Room room, InteractionWiredExtra definition) { + if (room == null || definition == null || room.getRoomSpecialTypes() == null) { + return null; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(definition.getX(), definition.getY()); + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (extra instanceof WiredExtraVariableLevelUpSystem) { + return (WiredExtraVariableLevelUpSystem) extra; + } + } + + return null; + } + + public static List getDerivedDefinitions(Room room, int targetType, InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo baseDefinition) { + if (room == null || definitionExtra == null || baseDefinition == null || !baseDefinition.hasValue()) { + return Collections.emptyList(); + } + + WiredExtraVariableLevelUpSystem levelSystem = getLevelSystem(room, definitionExtra); + if (levelSystem == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (int subvariableType : levelSystem.getSelectedSubvariables()) { + result.add(new WiredVariableDefinitionInfo( + createSyntheticItemId(targetType, baseDefinition.getItemId(), subvariableType), + baseDefinition.getName() + "." + getSubvariableKey(subvariableType), + true, + baseDefinition.getAvailability(), + false, + true + )); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public static WiredVariableDefinitionInfo getDerivedDefinitionInfo(Room room, int targetType, int syntheticItemId) { + DerivedDefinition derived = resolveDerivedDefinition(room, targetType, syntheticItemId); + + if (derived == null) { + return null; + } + + return new WiredVariableDefinitionInfo( + derived.syntheticItemId, + derived.variableName, + true, + derived.baseDefinition.getAvailability(), + false, + true + ); + } + + public static DerivedDefinition resolveDerivedDefinition(Room room, int targetType, int syntheticItemId) { + DecodedSyntheticId decoded = decodeSyntheticId(syntheticItemId); + if (decoded == null || decoded.targetType != targetType || room == null || room.getRoomSpecialTypes() == null) { + return null; + } + + InteractionWiredExtra baseExtra = room.getRoomSpecialTypes().getExtra(decoded.baseDefinitionItemId); + if (!matchesTarget(baseExtra, targetType)) { + return null; + } + + WiredVariableDefinitionInfo baseDefinition = createBaseDefinitionInfo(room, baseExtra, targetType); + if (baseDefinition == null || !baseDefinition.hasValue()) { + return null; + } + + WiredExtraVariableLevelUpSystem levelSystem = getLevelSystem(room, baseExtra); + if (levelSystem == null || !levelSystem.hasSubvariable(decoded.subvariableType)) { + return null; + } + + return new DerivedDefinition( + syntheticItemId, + decoded.baseDefinitionItemId, + decoded.subvariableType, + baseDefinition.getName() + "." + getSubvariableKey(decoded.subvariableType), + baseDefinition, + levelSystem + ); + } + + public static Integer getDerivedValue(WiredExtraVariableLevelUpSystem levelSystem, int subvariableType, Integer baseValue) { + if (levelSystem == null || baseValue == null) { + return null; + } + + LevelProgress progress = calculateProgress(levelSystem, baseValue); + return switch (subvariableType) { + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_LEVEL -> progress.currentLevel; + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_XP -> progress.currentXp; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS -> progress.progressXp; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS_PERCENT -> progress.progressPercent; + case WiredExtraVariableLevelUpSystem.SUB_TOTAL_XP_REQUIRED -> progress.totalXpRequired; + case WiredExtraVariableLevelUpSystem.SUB_XP_REMAINING -> progress.xpRemaining; + case WiredExtraVariableLevelUpSystem.SUB_IS_AT_MAX -> progress.isAtMax ? 1 : 0; + case WiredExtraVariableLevelUpSystem.SUB_MAX_LEVEL -> progress.maxLevel; + default -> null; + }; + } + + public static List buildPreviewEntries(WiredExtraVariableLevelUpSystem levelSystem) { + if (levelSystem == null) { + return Collections.emptyList(); + } + + return buildThresholdEntries(levelSystem); + } + + private static boolean matchesTarget(InteractionWiredExtra extra, int targetType) { + if (extra == null) { + return false; + } + + return switch (targetType) { + case TARGET_FURNI -> extra instanceof WiredExtraFurniVariable; + case TARGET_ROOM -> (extra instanceof WiredExtraRoomVariable) + || (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()); + default -> (extra instanceof WiredExtraUserVariable) + || (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()); + }; + } + + private static WiredVariableDefinitionInfo createBaseDefinitionInfo(Room room, InteractionWiredExtra extra, int targetType) { + if (room == null || extra == null) { + return null; + } + + if (targetType == TARGET_FURNI && extra instanceof WiredExtraFurniVariable) { + WiredExtraFurniVariable definition = (WiredExtraFurniVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(room, definition), + false + ); + } + + if (targetType == TARGET_USER) { + if (extra instanceof WiredExtraUserVariable) { + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + } + + if (targetType == TARGET_ROOM) { + if (extra instanceof WiredExtraRoomVariable) { + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + } + + return null; + } + + private static int createSyntheticItemId(int targetType, int baseDefinitionItemId, int subvariableType) { + int offset = switch (targetType) { + case TARGET_FURNI -> SYNTHETIC_FURNI_OFFSET; + case TARGET_ROOM -> SYNTHETIC_ROOM_OFFSET; + default -> SYNTHETIC_USER_OFFSET; + }; + + return offset + (baseDefinitionItemId * SYNTHETIC_STRIDE) + (subvariableType + 1); + } + + private static DecodedSyntheticId decodeSyntheticId(int syntheticItemId) { + if (syntheticItemId >= SYNTHETIC_ROOM_OFFSET) { + return decodeSyntheticId(syntheticItemId, TARGET_ROOM, SYNTHETIC_ROOM_OFFSET); + } + + if (syntheticItemId >= SYNTHETIC_FURNI_OFFSET) { + return decodeSyntheticId(syntheticItemId, TARGET_FURNI, SYNTHETIC_FURNI_OFFSET); + } + + if (syntheticItemId >= SYNTHETIC_USER_OFFSET) { + return decodeSyntheticId(syntheticItemId, TARGET_USER, SYNTHETIC_USER_OFFSET); + } + + return null; + } + + private static DecodedSyntheticId decodeSyntheticId(int syntheticItemId, int targetType, int offset) { + int localValue = syntheticItemId - offset; + if (localValue < 0) { + return null; + } + + int encodedSubvariable = localValue % SYNTHETIC_STRIDE; + int baseDefinitionItemId = localValue / SYNTHETIC_STRIDE; + int subvariableType = encodedSubvariable - 1; + + if (baseDefinitionItemId <= 0 || subvariableType < 0 || subvariableType >= WiredExtraVariableLevelUpSystem.SUBVARIABLE_COUNT) { + return null; + } + + return new DecodedSyntheticId(targetType, baseDefinitionItemId, subvariableType); + } + + private static String getSubvariableKey(int subvariableType) { + return switch (subvariableType) { + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_LEVEL -> "current_level"; + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_XP -> "current_xp"; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS -> "level_progress"; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS_PERCENT -> "level_progress_percent"; + case WiredExtraVariableLevelUpSystem.SUB_TOTAL_XP_REQUIRED -> "total_xp_required"; + case WiredExtraVariableLevelUpSystem.SUB_XP_REMAINING -> "xp_remaining"; + case WiredExtraVariableLevelUpSystem.SUB_IS_AT_MAX -> "is_at_max"; + case WiredExtraVariableLevelUpSystem.SUB_MAX_LEVEL -> "max_level"; + default -> "value"; + }; + } + + private static LevelProgress calculateProgress(WiredExtraVariableLevelUpSystem levelSystem, int rawBaseValue) { + int currentXp = Math.max(0, rawBaseValue); + List entries = buildThresholdEntries(levelSystem); + + if (entries.isEmpty()) { + entries = new ArrayList<>(); + entries.add(new LevelEntry(1, 0)); + } + + int maxLevel = entries.get(entries.size() - 1).level; + int currentLevel = 1; + int currentThreshold = 0; + int nextThreshold = 0; + + for (int index = 0; index < entries.size(); index++) { + LevelEntry entry = entries.get(index); + + if (currentXp >= entry.requiredXp) { + currentLevel = entry.level; + currentThreshold = entry.requiredXp; + nextThreshold = (index + 1 < entries.size()) ? entries.get(index + 1).requiredXp : entry.requiredXp; + continue; + } + + nextThreshold = entry.requiredXp; + break; + } + + boolean isAtMax = currentLevel >= maxLevel; + + if (isAtMax) { + nextThreshold = currentThreshold; + } + + int progressXp = Math.max(0, currentXp - currentThreshold); + int progressPercent; + + if (isAtMax) { + progressPercent = 100; + } else { + int delta = Math.max(0, nextThreshold - currentThreshold); + progressPercent = (delta <= 0) ? 100 : Math.max(0, Math.min(100, (int) Math.floor((progressXp * 100D) / delta))); + } + + int totalXpRequired = isAtMax ? currentThreshold : nextThreshold; + int xpRemaining = Math.max(0, totalXpRequired - currentXp); + + return new LevelProgress(currentLevel, currentXp, progressXp, progressPercent, totalXpRequired, xpRemaining, isAtMax, maxLevel); + } + + private static List buildThresholdEntries(WiredExtraVariableLevelUpSystem levelSystem) { + return switch (levelSystem.getMode()) { + case WiredExtraVariableLevelUpSystem.MODE_EXPONENTIAL -> buildExponentialEntries(levelSystem); + case WiredExtraVariableLevelUpSystem.MODE_MANUAL -> buildManualEntries(levelSystem); + default -> buildLinearEntries(levelSystem); + }; + } + + private static List buildLinearEntries(WiredExtraVariableLevelUpSystem levelSystem) { + List entries = new ArrayList<>(); + int maxLevel = Math.max(1, levelSystem.getMaxLevel()); + int stepSize = Math.max(0, levelSystem.getStepSize()); + + for (int level = 1; level <= maxLevel; level++) { + entries.add(new LevelEntry(level, clamp((long) (level - 1) * stepSize))); + } + + return entries; + } + + private static List buildExponentialEntries(WiredExtraVariableLevelUpSystem levelSystem) { + List entries = new ArrayList<>(); + int maxLevel = Math.max(1, levelSystem.getMaxLevel()); + int currentIncrement = Math.max(0, levelSystem.getFirstLevelXp()); + int factor = Math.max(0, levelSystem.getIncreaseFactor()); + long threshold = 0L; + + entries.add(new LevelEntry(1, 0)); + + for (int level = 2; level <= maxLevel; level++) { + threshold += currentIncrement; + entries.add(new LevelEntry(level, clamp(threshold))); + currentIncrement = clamp(Math.round(currentIncrement * (100D + factor) / 100D)); + } + + return entries; + } + + private static List buildManualEntries(WiredExtraVariableLevelUpSystem levelSystem) { + LinkedHashMap anchors = parseAnchors(levelSystem.getInterpolationText()); + if (!anchors.containsKey(1)) { + anchors.put(1, 0); + } + + List> sortedAnchors = new ArrayList<>(anchors.entrySet()); + sortedAnchors.sort(Map.Entry.comparingByKey()); + + if (sortedAnchors.isEmpty()) { + return Collections.singletonList(new LevelEntry(1, 0)); + } + + LinkedHashMap result = new LinkedHashMap<>(); + + for (int index = 0; index < sortedAnchors.size(); index++) { + Map.Entry current = sortedAnchors.get(index); + int currentLevel = Math.max(1, current.getKey()); + int currentXp = Math.max(0, current.getValue()); + + result.put(currentLevel, currentXp); + + if (index + 1 >= sortedAnchors.size()) { + continue; + } + + Map.Entry next = sortedAnchors.get(index + 1); + int nextLevel = Math.max(currentLevel, next.getKey()); + int nextXp = Math.max(0, next.getValue()); + + if (nextLevel <= currentLevel) { + continue; + } + + for (int level = currentLevel + 1; level < nextLevel; level++) { + double ratio = (double) (level - currentLevel) / (double) (nextLevel - currentLevel); + int interpolatedXp = clamp(Math.round(currentXp + ((nextXp - currentXp) * ratio))); + result.put(level, interpolatedXp); + } + } + + List entries = new ArrayList<>(); + for (Map.Entry entry : result.entrySet()) { + entries.add(new LevelEntry(entry.getKey(), entry.getValue())); + } + + entries.sort(Comparator.comparingInt(levelEntry -> levelEntry.level)); + return entries; + } + + private static LinkedHashMap parseAnchors(String interpolationText) { + LinkedHashMap result = new LinkedHashMap<>(); + if (interpolationText == null || interpolationText.trim().isEmpty()) { + return result; + } + + for (String rawLine : interpolationText.split("\n")) { + if (rawLine == null) { + continue; + } + + String line = rawLine.trim(); + if (line.isEmpty()) { + continue; + } + + int separatorIndex = line.indexOf('='); + if (separatorIndex < 0) { + separatorIndex = line.indexOf(','); + } + + if (separatorIndex <= 0) { + continue; + } + + Integer level = parseInteger(line.substring(0, separatorIndex)); + Integer xp = parseInteger(line.substring(separatorIndex + 1)); + + if (level == null || xp == null || level <= 0 || xp < 0) { + continue; + } + + result.put(level, xp); + } + + return result; + } + + private static Integer parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? null : Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static int clamp(long value) { + if (value > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + + if (value < Integer.MIN_VALUE) { + return Integer.MIN_VALUE; + } + + return (int) value; + } + + public static class DerivedDefinition { + private final int syntheticItemId; + private final int baseDefinitionItemId; + private final int subvariableType; + private final String variableName; + private final WiredVariableDefinitionInfo baseDefinition; + private final WiredExtraVariableLevelUpSystem levelSystem; + + public DerivedDefinition(int syntheticItemId, int baseDefinitionItemId, int subvariableType, String variableName, WiredVariableDefinitionInfo baseDefinition, WiredExtraVariableLevelUpSystem levelSystem) { + this.syntheticItemId = syntheticItemId; + this.baseDefinitionItemId = baseDefinitionItemId; + this.subvariableType = subvariableType; + this.variableName = variableName; + this.baseDefinition = baseDefinition; + this.levelSystem = levelSystem; + } + + public int getBaseDefinitionItemId() { + return this.baseDefinitionItemId; + } + + public int getSubvariableType() { + return this.subvariableType; + } + + public WiredVariableDefinitionInfo getBaseDefinition() { + return this.baseDefinition; + } + + public WiredExtraVariableLevelUpSystem getLevelSystem() { + return this.levelSystem; + } + } + + public static class LevelEntry { + private final int level; + private final int requiredXp; + + public LevelEntry(int level, int requiredXp) { + this.level = level; + this.requiredXp = requiredXp; + } + + public int getLevel() { + return this.level; + } + + public int getRequiredXp() { + return this.requiredXp; + } + } + + private static class LevelProgress { + private final int currentLevel; + private final int currentXp; + private final int progressXp; + private final int progressPercent; + private final int totalXpRequired; + private final int xpRemaining; + private final boolean isAtMax; + private final int maxLevel; + + private LevelProgress(int currentLevel, int currentXp, int progressXp, int progressPercent, int totalXpRequired, int xpRemaining, boolean isAtMax, int maxLevel) { + this.currentLevel = currentLevel; + this.currentXp = currentXp; + this.progressXp = progressXp; + this.progressPercent = progressPercent; + this.totalXpRequired = totalXpRequired; + this.xpRemaining = xpRemaining; + this.isAtMax = isAtMax; + this.maxLevel = maxLevel; + } + } + + private static class DecodedSyntheticId { + private final int targetType; + private final int baseDefinitionItemId; + private final int subvariableType; + + private DecodedSyntheticId(int targetType, int baseDefinitionItemId, int subvariableType) { + this.targetType = targetType; + this.baseDefinitionItemId = baseDefinitionItemId; + this.subvariableType = subvariableType; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java new file mode 100644 index 00000000..50aae6d9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java @@ -0,0 +1,61 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector; +import com.eu.habbo.habbohotel.rooms.Room; +import gnu.trove.set.hash.THashSet; + +public final class WiredVariableTextConnectorSupport { + private WiredVariableTextConnectorSupport() { + } + + public static boolean isTextConnected(Room room, InteractionWiredExtra definition) { + return getConnector(room, definition) != null; + } + + public static boolean isTextConnected(Room room, int definitionItemId) { + return getConnector(room, definitionItemId) != null; + } + + public static WiredExtraVariableTextConnector getConnector(Room room, int definitionItemId) { + if (room == null || room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(definitionItemId); + return getConnector(room, extra); + } + + public static WiredExtraVariableTextConnector getConnector(Room room, InteractionWiredExtra definition) { + if (room == null || definition == null || room.getRoomSpecialTypes() == null) { + return null; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(definition.getX(), definition.getY()); + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (extra instanceof WiredExtraVariableTextConnector) { + return (WiredExtraVariableTextConnector) extra; + } + } + + return null; + } + + public static String toText(Room room, int definitionItemId, Integer value) { + if (value == null) { + return ""; + } + + WiredExtraVariableTextConnector connector = getConnector(room, definitionItemId); + return connector != null ? connector.resolveText(value) : String.valueOf(value); + } + + public static Integer toValue(Room room, int definitionItemId, String text) { + WiredExtraVariableTextConnector connector = getConnector(room, definitionItemId); + return connector != null ? connector.resolveValue(text) : null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java index 155001ec..003e06a2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java @@ -167,9 +167,15 @@ public final class WiredEvents { * @return the event */ public static WiredEvent userSays(Room room, RoomUnit user, String message) { + return userSays(room, user, message, -1, -1); + } + + public static WiredEvent userSays(Room room, RoomUnit user, String message, int chatType, int chatStyle) { return WiredEvent.builder(WiredEvent.Type.USER_SAYS, room) .actor(user) .text(message) + .chatType(chatType) + .chatStyle(chatStyle) .tile(user.getCurrentLocation()) .build(); } @@ -192,6 +198,42 @@ public final class WiredEvents { .build(); } + public static WiredEvent userVariableChanged(Room room, RoomUnit user, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + return WiredEvent.builder(WiredEvent.Type.VARIABLE_CHANGED, room) + .actor(user) + .tile((user != null) ? user.getCurrentLocation() : null) + .variableTargetType(0) + .variableDefinitionItemId(definitionItemId) + .variableCreated(created) + .variableDeleted(deleted) + .variableChangeKind(changeKind) + .build(); + } + + public static WiredEvent furniVariableChanged(Room room, HabboItem item, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + RoomTile tile = (item != null) ? room.getLayout().getTile(item.getX(), item.getY()) : null; + + return WiredEvent.builder(WiredEvent.Type.VARIABLE_CHANGED, room) + .sourceItem(item) + .tile(tile) + .variableTargetType(1) + .variableDefinitionItemId(definitionItemId) + .variableCreated(created) + .variableDeleted(deleted) + .variableChangeKind(changeKind) + .build(); + } + + public static WiredEvent roomVariableChanged(Room room, int definitionItemId, WiredEvent.VariableChangeKind changeKind) { + return WiredEvent.builder(WiredEvent.Type.VARIABLE_CHANGED, room) + .variableTargetType(3) + .variableDefinitionItemId(definitionItemId) + .variableCreated(false) + .variableDeleted(false) + .variableChangeKind(changeKind) + .build(); + } + // ========== Timer Events ========== /** diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 18163a51..72fe9403 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -67,6 +67,12 @@ import com.eu.habbo.messages.incoming.users.*; import com.eu.habbo.messages.incoming.wired.WiredApplySetConditionsEvent; import com.eu.habbo.messages.incoming.wired.WiredConditionSaveDataEvent; import com.eu.habbo.messages.incoming.wired.WiredEffectSaveDataEvent; +import com.eu.habbo.messages.incoming.wired.WiredMonitorRequestEvent; +import com.eu.habbo.messages.incoming.wired.WiredRoomSettingsRequestEvent; +import com.eu.habbo.messages.incoming.wired.WiredRoomSettingsSaveEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserVariableManageEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserVariableUpdateEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserVariablesRequestEvent; import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveDataEvent; import com.eu.habbo.plugin.EventHandler; import com.eu.habbo.plugin.events.emulator.EmulatorConfigUpdatedEvent; @@ -615,6 +621,12 @@ public class PacketManager { this.registerHandler(Incoming.WiredEffectSaveDataEvent, WiredEffectSaveDataEvent.class); this.registerHandler(Incoming.WiredConditionSaveDataEvent, WiredConditionSaveDataEvent.class); this.registerHandler(Incoming.WiredApplySetConditionsEvent, WiredApplySetConditionsEvent.class); + this.registerHandler(Incoming.WiredMonitorRequestEvent, WiredMonitorRequestEvent.class); + this.registerHandler(Incoming.WiredRoomSettingsRequestEvent, WiredRoomSettingsRequestEvent.class); + this.registerHandler(Incoming.WiredRoomSettingsSaveEvent, WiredRoomSettingsSaveEvent.class); + this.registerHandler(Incoming.WiredUserVariablesRequestEvent, WiredUserVariablesRequestEvent.class); + this.registerHandler(Incoming.WiredUserVariableUpdateEvent, WiredUserVariableUpdateEvent.class); + this.registerHandler(Incoming.WiredUserVariableManageEvent, WiredUserVariableManageEvent.class); } void registerUnknown() throws Exception { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 9b99e7ab..4f8554cf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -409,6 +409,12 @@ public class Incoming { // CUSTOM public static final int UpdateFurniturePositionEvent = 10019; public static final int ClickUserEvent = 10020; + public static final int WiredMonitorRequestEvent = 10021; + public static final int WiredRoomSettingsRequestEvent = 10022; + public static final int WiredRoomSettingsSaveEvent = 10023; + public static final int WiredUserVariablesRequestEvent = 10024; + public static final int WiredUserVariableUpdateEvent = 10025; + public static final int WiredUserVariableManageEvent = 10026; public static final int RequestInventoryPetDelete = 10030; public static final int RequestInventoryBadgeDelete = 10031; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java index 90365132..28bceeb0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java @@ -41,7 +41,7 @@ public class WiredApplySetConditionsEvent extends MessageHandler { if (room != null) { // Executing Habbo should be able to edit wireds - if (room.hasRights(this.client.getHabbo()) || room.isOwner(this.client.getHabbo())) { + if (room.canModifyWired(this.client.getHabbo())) { List wireds = new ArrayList<>(); wireds.addAll(room.getRoomSpecialTypes().getConditions()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java index b5e51094..c1ca5c37 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java @@ -4,7 +4,6 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWired; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; @@ -23,7 +22,7 @@ public class WiredConditionSaveDataEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { - if (room.hasRights(this.client.getHabbo()) || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER) || this.client.getHabbo().hasPermission(Permission.ACC_MOVEROTATE)) { + if (room.canModifyWired(this.client.getHabbo())) { InteractionWiredCondition condition = room.getRoomSpecialTypes().getCondition(itemId); if (condition != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java index 265a3a07..258e02ce 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java @@ -5,7 +5,6 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWired; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; @@ -20,7 +19,7 @@ public class WiredEffectSaveDataEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { - if (room.hasRights(this.client.getHabbo()) || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER) || this.client.getHabbo().hasPermission(Permission.ACC_MOVEROTATE)) { + if (room.canModifyWired(this.client.getHabbo())) { InteractionWiredEffect effect = room.getRoomSpecialTypes().getEffect(itemId); InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(itemId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredMonitorRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredMonitorRequestEvent.java new file mode 100644 index 00000000..2be8c2c7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredMonitorRequestEvent.java @@ -0,0 +1,41 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wired.WiredMonitorDataComposer; + +public class WiredMonitorRequestEvent extends MessageHandler { + private static final int ACTION_FETCH = 0; + private static final int ACTION_CLEAR_LOGS = 1; + + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canInspectWired(this.client.getHabbo())) { + return; + } + + int action = ACTION_FETCH; + + if (this.packet.bytesAvailable() >= 4) { + action = this.packet.readInt(); + } + + if ((action == ACTION_CLEAR_LOGS) && room.canModifyWired(this.client.getHabbo())) { + WiredManager.clearDiagnosticsLogs(room.getId()); + } + + this.client.sendResponse(new WiredMonitorDataComposer(WiredManager.getDiagnosticsSnapshot(room.getId()))); + } + + @Override + public int getRatelimit() { + return 50; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsRequestEvent.java new file mode 100644 index 00000000..3275b079 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsRequestEvent.java @@ -0,0 +1,23 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wired.WiredRoomSettingsDataComposer; + +public class WiredRoomSettingsRequestEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + } + + @Override + public int getRatelimit() { + return 250; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsSaveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsSaveEvent.java new file mode 100644 index 00000000..a58246f7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsSaveEvent.java @@ -0,0 +1,38 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wired.WiredRoomSettingsDataComposer; + +public class WiredRoomSettingsSaveEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (this.packet.bytesAvailable() < 8) { + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + return; + } + + if (!room.canManageWiredSettings(this.client.getHabbo())) { + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + return; + } + + int inspectMask = this.packet.readInt(); + int modifyMask = this.packet.readInt(); + + room.saveWiredSettings(inspectMask, modifyMask); + + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + } + + @Override + public int getRatelimit() { + return 250; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java index dfa480a3..66970dd2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java @@ -4,7 +4,6 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWired; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; @@ -19,7 +18,7 @@ public class WiredTriggerSaveDataEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { - if (room.hasRights(this.client.getHabbo()) || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER) || this.client.getHabbo().hasPermission(Permission.ACC_MOVEROTATE)) { + if (room.canModifyWired(this.client.getHabbo())) { InteractionWiredTrigger trigger = room.getRoomSpecialTypes().getTrigger(itemId); if (trigger != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableManageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableManageEvent.java new file mode 100644 index 00000000..ca6ab112 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableManageEvent.java @@ -0,0 +1,74 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserVariableManageEvent extends MessageHandler { + private static final int ACTION_ASSIGN = 0; + private static final int ACTION_REMOVE = 1; + private static final int TARGET_ROOM = 3; + + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canModifyWired(this.client.getHabbo())) { + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + if (this.packet.bytesAvailable() < 20) { + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + int action = this.packet.readInt(); + int targetType = this.packet.readInt(); + int targetId = this.packet.readInt(); + int definitionItemId = this.packet.readInt(); + int value = this.packet.readInt(); + + switch (targetType) { + case com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveVariable.TARGET_FURNI: + if (action == ACTION_REMOVE) { + room.getFurniVariableManager().removeVariable(targetId, definitionItemId); + } else { + HabboItem furni = room.getHabboItem(targetId); + if (furni != null) { + room.getFurniVariableManager().assignVariable(furni, definitionItemId, value, true); + } + } + break; + case TARGET_ROOM: + if (action == ACTION_REMOVE) { + room.getRoomVariableManager().removeVariable(definitionItemId); + } else { + room.getRoomVariableManager().updateVariableValue(definitionItemId, value); + } + break; + default: + if (action == ACTION_REMOVE) { + room.getUserVariableManager().removeVariable(targetId, definitionItemId); + } else { + Habbo habbo = room.getHabbo(targetId); + if (habbo != null) { + room.getUserVariableManager().assignVariable(habbo, definitionItemId, value, true); + } + } + break; + } + + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + } + + @Override + public int getRatelimit() { + return 150; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableUpdateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableUpdateEvent.java new file mode 100644 index 00000000..9475a014 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableUpdateEvent.java @@ -0,0 +1,53 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveVariable; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserVariableUpdateEvent extends MessageHandler { + private static final int TARGET_ROOM = 3; + + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canModifyWired(this.client.getHabbo())) { + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + if (this.packet.bytesAvailable() < 16) { + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + int targetType = this.packet.readInt(); + int targetId = this.packet.readInt(); + int definitionItemId = this.packet.readInt(); + int value = this.packet.readInt(); + + if (targetType == WiredEffectGiveVariable.TARGET_FURNI) { + room.getFurniVariableManager().updateVariableValue(targetId, definitionItemId, value); + room.getFurniVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + if (targetType == TARGET_ROOM) { + room.getRoomVariableManager().updateVariableValue(definitionItemId, value); + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + room.getUserVariableManager().updateVariableValue(targetId, definitionItemId, value); + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + } + + @Override + public int getRatelimit() { + return 150; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariablesRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariablesRequestEvent.java new file mode 100644 index 00000000..81ea1c93 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariablesRequestEvent.java @@ -0,0 +1,22 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserVariablesRequestEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + } + + @Override + public int getRatelimit() { + return 50; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index f354973e..3abf3fbc 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -120,6 +120,9 @@ public class Outgoing { public final static int EnableNotificationsComposer = 3284; // PRODUCTION-201611291003-338511768 public final static int HallOfFameComposer = 3005; // PRODUCTION-201611291003-338511768 public final static int WiredSavedComposer = 1155; // PRODUCTION-201611291003-338511768 + public final static int WiredMonitorDataComposer = 5101; // CUSTOM + public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM + public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768 public final static int MarketplaceConfigComposer = 1823; // PRODUCTION-201611291003-338511768 public final static int AddBotComposer = 1352; // PRODUCTION-201611291003-338511768 diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java index 5eba2421..4772d455 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java @@ -18,9 +18,11 @@ public class ModToolUserInfoComposer extends MessageComposer { private static final Logger LOGGER = LoggerFactory.getLogger(ModToolUserInfoComposer.class); private final ResultSet set; + private final boolean hideMail; - public ModToolUserInfoComposer(ResultSet set) { + public ModToolUserInfoComposer(ResultSet set, boolean hideMail) { this.set = set; + this.hideMail = hideMail; } @Override @@ -58,7 +60,7 @@ public class ModToolUserInfoComposer extends MessageComposer { this.response.appendString(""); //Last Purchase Timestamp this.response.appendInt(this.set.getInt("user_id")); //Personal Identification # this.response.appendInt(0); // Number of account bans - this.response.appendString(this.set.getBoolean("hide_mail") ? "" : this.set.getString("mail")); + this.response.appendString(this.hideMail ? "" : this.set.getString("mail")); this.response.appendString("Rank (" + this.set.getInt("rank_id") + "): " + this.set.getString("rank_name")); //user_class_txt ModToolSanctions modToolSanctions = Emulator.getGameEnvironment().getModToolSanctions(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java index 29414eb0..d95cad7c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java @@ -5,7 +5,9 @@ import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.UUID; public class WiredMovementsComposer extends MessageComposer { public static final int TYPE_USER_MOVE = 0; @@ -24,7 +26,7 @@ public class WiredMovementsComposer extends MessageComposer { private final List movements; public WiredMovementsComposer(List movements) { - this.movements = movements == null ? new ArrayList<>() : movements; + this.movements = normalizeMovements(movements == null ? new ArrayList<>() : movements); } @Override @@ -40,6 +42,107 @@ public class WiredMovementsComposer extends MessageComposer { return this.response; } + private static List normalizeMovements(List source) + { + if((source == null) || source.isEmpty()) return new ArrayList<>(); + + final LinkedHashMap normalized = new LinkedHashMap<>(); + + for(final MovementData movement : source) + { + if(movement == null) continue; + + final String key = movementKey(movement); + + if(key == null) + { + normalized.put(UUID.randomUUID().toString(), movement); + continue; + } + + final MovementData existing = normalized.get(key); + + if(existing == null) + { + normalized.put(key, movement); + continue; + } + + normalized.put(key, mergeMovement(existing, movement)); + } + + return new ArrayList<>(normalized.values()); + } + + private static String movementKey(MovementData movement) + { + if(movement instanceof FurniMovementData) + { + return "furni:" + ((FurniMovementData) movement).id; + } + + if(movement instanceof UserMovementData) + { + return "user:" + ((UserMovementData) movement).id; + } + + if(movement instanceof UserDirectionData) + { + return "userdir:" + ((UserDirectionData) movement).id; + } + + if(movement instanceof WallItemMovementData) + { + return "wall:" + ((WallItemMovementData) movement).id; + } + + return null; + } + + private static MovementData mergeMovement(MovementData previous, MovementData current) + { + if((previous instanceof FurniMovementData) && (current instanceof FurniMovementData)) + { + final FurniMovementData oldMovement = (FurniMovementData) previous; + final FurniMovementData newMovement = (FurniMovementData) current; + + return furniMovement( + oldMovement.id, + oldMovement.fromX, + oldMovement.fromY, + newMovement.toX, + newMovement.toY, + oldMovement.fromZ, + newMovement.toZ, + newMovement.rotation, + newMovement.duration, + newMovement.elapsed, + newMovement.anchorType, + newMovement.anchorId); + } + + if((previous instanceof UserMovementData) && (current instanceof UserMovementData)) + { + final UserMovementData oldMovement = (UserMovementData) previous; + final UserMovementData newMovement = (UserMovementData) current; + + return new UserMovementData( + oldMovement.id, + oldMovement.fromX, + oldMovement.fromY, + newMovement.toX, + newMovement.toY, + oldMovement.fromZ, + newMovement.toZ, + newMovement.movementType, + newMovement.bodyDirection, + newMovement.headDirection, + newMovement.duration); + } + + return current; + } + public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ) { return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, 0, DEFAULT_DURATION, 0, FURNI_ANCHOR_NONE, 0); } @@ -92,13 +195,13 @@ public class WiredMovementsComposer extends MessageComposer { } private static final class UserMovementData extends BaseMovementData { + private final int id; private final int fromX; private final int fromY; private final int toX; private final int toY; private final double fromZ; private final double toZ; - private final int id; private final int movementType; private final int bodyDirection; private final int headDirection; @@ -136,13 +239,13 @@ public class WiredMovementsComposer extends MessageComposer { } private static final class FurniMovementData extends BaseMovementData { + private final int id; private final int fromX; private final int fromY; private final int toX; private final int toY; private final double fromZ; private final double toZ; - private final int id; private final int rotation; private final int duration; private final int elapsed; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java new file mode 100644 index 00000000..6f27665d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java @@ -0,0 +1,84 @@ +package com.eu.habbo.messages.outgoing.wired; + +import com.eu.habbo.habbohotel.wired.core.WiredRoomDiagnostics; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class WiredMonitorDataComposer extends MessageComposer { + private final WiredRoomDiagnostics.Snapshot snapshot; + + public WiredMonitorDataComposer(WiredRoomDiagnostics.Snapshot snapshot) { + this.snapshot = snapshot; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredMonitorDataComposer); + + if (this.snapshot == null) { + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendBoolean(false); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + return this.response; + } + + this.response.appendInt(this.snapshot.getUsageCurrentWindow()); + this.response.appendInt(this.snapshot.getUsageLimitPerWindow()); + this.response.appendBoolean(this.snapshot.isHeavy()); + this.response.appendInt(this.snapshot.getDelayedEventsPending()); + this.response.appendInt(this.snapshot.getDelayedEventsLimit()); + this.response.appendInt(this.snapshot.getAverageExecutionMs()); + this.response.appendInt(this.snapshot.getPeakExecutionMs()); + this.response.appendInt(this.snapshot.getRecursionDepthCurrent()); + this.response.appendInt(this.snapshot.getRecursionDepthLimit()); + this.response.appendInt(this.snapshot.getKilledRemainingSeconds()); + this.response.appendInt(this.snapshot.getUsageWindowMs()); + this.response.appendInt(this.snapshot.getOverloadAverageThresholdMs()); + this.response.appendInt(this.snapshot.getOverloadPeakThresholdMs()); + this.response.appendInt(this.snapshot.getHeavyUsageThresholdPercent()); + this.response.appendInt(this.snapshot.getHeavyConsecutiveWindowsThreshold()); + this.response.appendInt(this.snapshot.getOverloadConsecutiveWindowsThreshold()); + this.response.appendInt(this.snapshot.getHeavyDelayedThresholdPercent()); + this.response.appendInt(this.snapshot.getLogs().size()); + + for (WiredRoomDiagnostics.LogEntry log : this.snapshot.getLogs()) { + this.response.appendString(log.getType().name()); + this.response.appendString(log.getSeverity().name()); + this.response.appendInt(log.getCount()); + this.response.appendInt((int) (log.getLastOccurredAtMs() / 1000L)); + this.response.appendString(log.getLatestReason()); + this.response.appendString(log.getLatestSourceLabel()); + this.response.appendInt(log.getLatestSourceId()); + } + + this.response.appendInt(this.snapshot.getHistory().size()); + + for (WiredRoomDiagnostics.HistoryEntry historyEntry : this.snapshot.getHistory()) { + this.response.appendString(historyEntry.getType().name()); + this.response.appendString(historyEntry.getSeverity().name()); + this.response.appendInt((int) (historyEntry.getOccurredAtMs() / 1000L)); + this.response.appendString(historyEntry.getReason()); + this.response.appendString(historyEntry.getSourceLabel()); + this.response.appendInt(historyEntry.getSourceId()); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredRoomSettingsDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredRoomSettingsDataComposer.java new file mode 100644 index 00000000..caf0dbd5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredRoomSettingsDataComposer.java @@ -0,0 +1,38 @@ +package com.eu.habbo.messages.outgoing.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class WiredRoomSettingsDataComposer extends MessageComposer { + private final Room room; + private final Habbo habbo; + + public WiredRoomSettingsDataComposer(Room room, Habbo habbo) { + this.room = room; + this.habbo = habbo; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredRoomSettingsDataComposer); + + int roomId = (this.room != null) ? this.room.getId() : 0; + boolean canInspect = this.room != null && this.room.canInspectWired(this.habbo); + boolean canModify = this.room != null && this.room.canModifyWired(this.habbo); + boolean canManageSettings = this.room != null && this.room.canManageWiredSettings(this.habbo); + int inspectMask = canInspect ? this.room.getWiredInspectMask() : 0; + int modifyMask = canInspect ? this.room.getWiredModifyMask() : 0; + + this.response.appendInt(roomId); + this.response.appendInt(inspectMask); + this.response.appendInt(modifyMask); + this.response.appendBoolean(canInspect); + this.response.appendBoolean(canModify); + this.response.appendBoolean(canManageSettings); + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredUserVariablesDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredUserVariablesDataComposer.java new file mode 100644 index 00000000..d0f50d0b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredUserVariablesDataComposer.java @@ -0,0 +1,171 @@ +package com.eu.habbo.messages.outgoing.wired; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomFurniVariableManager; +import com.eu.habbo.habbohotel.rooms.RoomVariableManager; +import com.eu.habbo.habbohotel.rooms.RoomUserVariableManager; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.Collections; +import java.util.List; + +public class WiredUserVariablesDataComposer extends MessageComposer { + private final RoomUserVariableManager.Snapshot userSnapshot; + private final RoomFurniVariableManager.Snapshot furniSnapshot; + private final RoomVariableManager.Snapshot roomSnapshot; + private final List contextDefinitions; + + public WiredUserVariablesDataComposer(RoomUserVariableManager.Snapshot userSnapshot, RoomFurniVariableManager.Snapshot furniSnapshot, RoomVariableManager.Snapshot roomSnapshot) { + this(userSnapshot, furniSnapshot, roomSnapshot, resolveContextDefinitions(userSnapshot, furniSnapshot, roomSnapshot)); + } + + public WiredUserVariablesDataComposer(RoomUserVariableManager.Snapshot userSnapshot, RoomFurniVariableManager.Snapshot furniSnapshot, RoomVariableManager.Snapshot roomSnapshot, List contextDefinitions) { + this.userSnapshot = userSnapshot; + this.furniSnapshot = furniSnapshot; + this.roomSnapshot = roomSnapshot; + this.contextDefinitions = (contextDefinitions != null) ? contextDefinitions : Collections.emptyList(); + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredUserVariablesDataComposer); + + int roomId = 0; + + if (this.userSnapshot != null) { + roomId = this.userSnapshot.getRoomId(); + } else if (this.furniSnapshot != null) { + roomId = this.furniSnapshot.getRoomId(); + } else if (this.roomSnapshot != null) { + roomId = this.roomSnapshot.getRoomId(); + } + + this.response.appendInt(roomId); + + this.response.appendInt((this.userSnapshot != null) ? this.userSnapshot.getDefinitions().size() : 0); + + if (this.userSnapshot != null) { + for (RoomUserVariableManager.DefinitionEntry definition : this.userSnapshot.getDefinitions()) { + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + } + + this.response.appendInt((this.userSnapshot != null) ? this.userSnapshot.getUsers().size() : 0); + + if (this.userSnapshot != null) { + for (RoomUserVariableManager.UserAssignmentsEntry user : this.userSnapshot.getUsers()) { + this.response.appendInt(user.getUserId()); + this.response.appendInt(user.getAssignments().size()); + + for (RoomUserVariableManager.AssignmentEntry assignment : user.getAssignments()) { + this.response.appendInt(assignment.getVariableItemId()); + this.response.appendBoolean(assignment.hasValue()); + this.response.appendInt((assignment.getValue() != null) ? assignment.getValue() : 0); + this.response.appendInt(assignment.getCreatedAt()); + this.response.appendInt(assignment.getUpdatedAt()); + } + } + } + + this.response.appendInt((this.furniSnapshot != null) ? this.furniSnapshot.getDefinitions().size() : 0); + + if (this.furniSnapshot != null) { + for (RoomFurniVariableManager.DefinitionEntry definition : this.furniSnapshot.getDefinitions()) { + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + } + + this.response.appendInt((this.furniSnapshot != null) ? this.furniSnapshot.getFurnis().size() : 0); + + if (this.furniSnapshot != null) { + for (RoomFurniVariableManager.FurniAssignmentsEntry furni : this.furniSnapshot.getFurnis()) { + this.response.appendInt(furni.getFurniId()); + this.response.appendInt(furni.getAssignments().size()); + + for (RoomFurniVariableManager.AssignmentEntry assignment : furni.getAssignments()) { + this.response.appendInt(assignment.getVariableItemId()); + this.response.appendBoolean(assignment.hasValue()); + this.response.appendInt((assignment.getValue() != null) ? assignment.getValue() : 0); + this.response.appendInt(assignment.getCreatedAt()); + this.response.appendInt(assignment.getUpdatedAt()); + } + } + } + + this.response.appendInt((this.roomSnapshot != null) ? this.roomSnapshot.getDefinitions().size() : 0); + + if (this.roomSnapshot != null) { + for (RoomVariableManager.DefinitionEntry definition : this.roomSnapshot.getDefinitions()) { + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + } + + this.response.appendInt((this.roomSnapshot != null) ? this.roomSnapshot.getAssignments().size() : 0); + + if (this.roomSnapshot != null) { + for (RoomVariableManager.AssignmentEntry assignment : this.roomSnapshot.getAssignments()) { + this.response.appendInt(assignment.getVariableItemId()); + this.response.appendBoolean(assignment.hasValue()); + this.response.appendInt((assignment.getValue() != null) ? assignment.getValue() : 0); + this.response.appendInt(assignment.getCreatedAt()); + this.response.appendInt(assignment.getUpdatedAt()); + } + } + + this.response.appendInt(this.contextDefinitions.size()); + + for (WiredVariableDefinitionInfo definition : this.contextDefinitions) { + if (definition == null) { + continue; + } + + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + + return this.response; + } + + private static List resolveContextDefinitions(RoomUserVariableManager.Snapshot userSnapshot, RoomFurniVariableManager.Snapshot furniSnapshot, RoomVariableManager.Snapshot roomSnapshot) { + int roomId = 0; + + if (userSnapshot != null) { + roomId = userSnapshot.getRoomId(); + } else if (furniSnapshot != null) { + roomId = furniSnapshot.getRoomId(); + } else if (roomSnapshot != null) { + roomId = roomSnapshot.getRoomId(); + } + + if (roomId <= 0) { + return Collections.emptyList(); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); + return room != null ? WiredContextVariableSupport.createDefinitionInfos(room) : Collections.emptyList(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java index d4bbc1c7..5712a3b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java +++ b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java @@ -120,6 +120,20 @@ public class PluginManager { WiredEngine.MAX_EVENTS_PER_WINDOW = Emulator.getConfig().getInt("wired.abuse.max.events.per.window", 100); WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000); WiredEngine.WIRED_BAN_DURATION_MS = Emulator.getConfig().getInt("wired.abuse.ban.duration.ms", 600000); + WiredEngine.MONITOR_USAGE_WINDOW_MS = Emulator.getConfig().getInt("wired.monitor.usage.window.ms", 1000); + WiredEngine.MONITOR_USAGE_LIMIT = Emulator.getConfig().getInt("wired.monitor.usage.limit", 1000); + WiredEngine.MONITOR_DELAYED_EVENTS_LIMIT = Emulator.getConfig().getInt("wired.monitor.delayed.events.limit", 100); + WiredEngine.MONITOR_OVERLOAD_AVERAGE_MS = Emulator.getConfig().getInt("wired.monitor.overload.average.ms", 50); + WiredEngine.MONITOR_OVERLOAD_PEAK_MS = Emulator.getConfig().getInt("wired.monitor.overload.peak.ms", 150); + WiredEngine.MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS = Emulator.getConfig().getInt("wired.monitor.overload.consecutive.windows", 2); + WiredEngine.MONITOR_HEAVY_USAGE_PERCENT = Emulator.getConfig().getInt("wired.monitor.heavy.usage.percent", 70); + WiredEngine.MONITOR_HEAVY_CONSECUTIVE_WINDOWS = Emulator.getConfig().getInt("wired.monitor.heavy.consecutive.windows", 5); + WiredEngine.MONITOR_HEAVY_DELAYED_PERCENT = Emulator.getConfig().getInt("wired.monitor.heavy.delayed.percent", 60); + + if (WiredManager.getEngine() != null) { + WiredManager.getEngine().clearAllDiagnostics(); + } + NavigatorManager.MAXIMUM_RESULTS_PER_PAGE = Emulator.getConfig().getInt("hotel.navigator.search.maxresults"); NavigatorManager.CATEGORY_SORT_USING_ORDER_NUM = Emulator.getConfig().getBoolean("hotel.navigator.sort.ordernum"); RoomChatMessage.MAXIMUM_LENGTH = Emulator.getConfig().getInt("hotel.chat.max.length"); diff --git a/docs/emulator_settings_reference.md b/docs/emulator_settings_reference.md new file mode 100644 index 00000000..a8911c7e --- /dev/null +++ b/docs/emulator_settings_reference.md @@ -0,0 +1,781 @@ +# Emulator Settings Reference + +## Scope + +This document inventories the non-wired keys currently stored in `emulator_settings` based on `Default Database/FullDB.sql`. Wired-specific keys are documented separately in `docs/wired_tools_reference.md`. + +Each entry below mirrors the comment written by `Database Updates/003_add_comment_column_to_emulator_settings.sql`, so the documentation and in-database comments stay aligned. + +## Table schema + +```sql +CREATE TABLE `emulator_settings` ( + `key` varchar(100) NOT NULL, + `value` varchar(512) NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`key`) +); +``` + +## Inventory summary + +- Total non-wired keys documented here: `329` +- Source of defaults: `Default Database/FullDB.sql` +- Value type is inferred from the default string stored in SQL. + +## Group index + +- `allowed` (1) +- `apollyon` (1) +- `basejump` (2) +- `bots` (1) +- `bubblealerts` (6) +- `bundle` (2) +- `callback` (3) +- `camera` (11) +- `catalog` (5) +- `commands` (3) +- `console` (1) +- `custom` (1) +- `db` (4) +- `debug` (7) +- `discount` (5) +- `easter_eggs` (1) +- `enc` (4) +- `essentials` (2) +- `flood` (1) +- `ftp` (4) +- `furniture` (1) +- `gamecenter` (16) +- `gamedata` (1) +- `guardians` (5) +- `hotel` (169) +- `hotelview` (5) +- `imager` (6) +- `images` (2) +- `info` (1) +- `invisible` (1) +- `io` (3) +- `logging` (6) +- `marketplace` (1) +- `monsterplant` (2) +- `moodlight` (1) +- `navigator` (1) +- `networking` (1) +- `notify` (1) +- `path` (1) +- `pathfinder` (4) +- `pirate_parrot` (2) +- `postit` (1) +- `pyramids` (1) +- `retro` (1) +- `room` (4) +- `rosie` (2) +- `runtime` (1) +- `save` (2) +- `scripter` (1) +- `seasonal` (7) +- `subscriptions` (12) +- `team` (1) +- `youtube` (1) + +## `allowed` + +Validation rules for usernames and account-facing inputs. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `allowed.username.characters` | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=!?@:,.` | `list` | Characters allowed when users choose or change a username. | + +## `apollyon` + +Custom project-specific behaviour switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `apollyon.cooldown.amount` | `250` | `integer` | Cooldown in milliseconds used by the Apollyon-specific behaviour or command flow. | + +## `basejump` + +BaseJump or FastFood launcher URLs. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `basejump.assets.url` | `http://localhost/gamecenter/gamecenter_basejump/BasicAssets.swf` | `url` | Asset URL used by the BaseJump or FastFood game client. | +| `basejump.url` | `http://localhost/game/BaseJump.swf` | `url` | SWF URL used to launch the BaseJump or FastFood game client. | + +## `bots` + +Miscellaneous visitor-bot display settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `bots.visitor.dateformat` | `yyyy-mm-dd HH:mm` | `string` | Date format used by visitor bots when they print timestamps. | + +## `bubblealerts` + +Bubble notification behaviour and assets. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `bubblealerts.enabled` | `1` | `boolean` | Master switch for bubble alert notifications. | +| `bubblealerts.notif_friendonline.enabled` | `1` | `boolean` | Enable bubble alerts when friends come online. | +| `bubblealerts.notif_friendonline.image` | `${image.library.url}notifications/figure?p=%figure%` | `template` | Image template used when showing friend-online bubble alerts. | +| `bubblealerts.notif_friendonline.useimage` | `1` | `boolean` | Use the configured figure image inside friend-online bubble alerts. | +| `bubblealerts.notif_marketplace.enabled` | `1` | `boolean` | Show bubble alerts for marketplace notifications. | +| `bubblealerts.notif_purchase.limited` | `0` | `boolean` | Show bubble alerts for limited-item purchases. | + +## `bundle` + +Bundle-specific toggles for pets and bots. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `bundle.bots.enabled` | `1` | `boolean` | Allow bots to be included in room bundles or package rewards. | +| `bundle.pets.enabled` | `1` | `boolean` | Allow pets to be included in room bundles or package rewards. | + +## `callback` + +HTTP callback integrations for external services. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `callback.get.version` | `1` | `boolean` | Enable the GET callback used to report version to external services. | +| `callback.post.errors` | `1` | `boolean` | Enable the POST callback used to report errors to external services. | +| `callback.post.statistics` | `1` | `boolean` | Enable the POST callback used to report statistics to external services. | + +## `camera` + +Camera costs, storage and publish settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `camera.enabled` | `1` | `boolean` | Enable the in-room camera feature. | +| `camera.extradata` | `{\"t\":%timestamp%, \"u\":\"%id%\", \"s\":%room_id%, \"w\":\"%url%\"}` | `template` | Extradata template written into camera photo items when they are created. | +| `camera.item_id` | `45970` | `integer` | Base item ID used by the generated camera photo furniture. | +| `camera.price.credits` | `2` | `integer` | Credit price charged when taking a camera photo. | +| `camera.price.points` | `0` | `boolean` | Amount of activity points charged when taking a camera photo. | +| `camera.price.points.publish` | `10` | `integer` | Amount of activity points charged when publishing a camera photo. | +| `camera.price.points.publish.type` | `0` | `boolean` | Activity point type used for the camera publish cost. | +| `camera.price.points.type` | `0` | `boolean` | Activity point type used for the camera capture cost. | +| `camera.publish.delay` | `180` | `integer` | Delay in seconds before a published camera photo becomes available. | +| `camera.url` | `http://localhost/usercontent/camera/` | `url` | Base URL where camera images are published. | +| `camera.use.https` | `1` | `boolean` | Force HTTPS when generating camera image URLs. | + +## `catalog` + +Catalog behaviour that is not wired-specific. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `catalog.guild.hc_required` | `1` | `boolean` | Require HC or VIP status before users can create a guild. | +| `catalog.guild.price` | `10` | `integer` | Credit cost required to create a guild. | +| `catalog.ltd.page.soldout` | `761` | `integer` | Layout or image ID used when a limited page is sold out. | +| `catalog.ltd.random` | `1` | `boolean` | Randomize the order or selection of limited catalog items. | +| `catalog.page.vipgifts` | `0` | `boolean` | Catalog page ID used for VIP gift redemption. | + +## `commands` + +Command-specific restrictions and compatibility flags. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `commands.cmd_chatcolor.banned_numbers` | `23;33;34` | `list` | Semicolon-separated list of chat color IDs blocked for the chatcolor command. | +| `commands.cmd_staffonline.min_rank` | `2` | `integer` | Minimum permission rank required to use the staffonline command. | +| `commands.plugins.oldstyle` | `0` | `boolean` | Use the legacy command plugin loading style. | + +## `console` + +Console behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `console.mode` | `1` | `boolean` | Controls the emulator console mode or console output style. | + +## `custom` + +Fork-specific custom behaviour switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `custom.stacking.enabled` | `0` | `boolean` | Enable custom item stacking behaviour outside the default stacking rules. | + +## `db` + +Database pooling and batching controls. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `db.max.partition.size` | `2` | `integer` | Maximum batch or partition size used by partitioned database operations. | +| `db.min.partition.size` | `1` | `boolean` | Minimum batch or partition size used by partitioned database operations. | +| `db.pool.maxsize` | `12` | `integer` | Maximum size of the database connection pool. | +| `db.pool.minsize` | `8` | `integer` | Minimum number of open connections kept in the database pool. | + +## `debug` + +Verbose debug output toggles. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `debug.mode` | `1` | `boolean` | Enable general emulator debug mode. | +| `debug.show.errors` | `1` | `boolean` | Show internal debug error messages. | +| `debug.show.headers` | `0` | `boolean` | Show packet headers in debug logs. | +| `debug.show.packets` | `0` | `boolean` | Print packet-level debug output. | +| `debug.show.packets.undefined` | `0` | `boolean` | Print debug output for undefined incoming or outgoing packets. | +| `debug.show.sql.exception` | `1` | `boolean` | Log SQL exceptions to the console. | +| `debug.show.users` | `1` | `boolean` | Show user-related debug messages. | + +## `discount` + +Discount batch rules for catalog purchases. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `discount.additional.thresholds` | `40;99` | `list` | Semicolon-separated discount thresholds used for extra batch bonuses. | +| `discount.batch.free.items` | `1` | `boolean` | Number of free items granted inside one discount batch. | +| `discount.batch.size` | `6` | `integer` | Number of items required for one discount batch. | +| `discount.bonus.min.discounts` | `1` | `boolean` | Minimum number of discount batches required before the bonus logic applies. | +| `discount.max.allowed.items` | `100` | `integer` | Maximum number of catalog items that can participate in one discount batch. | + +## `easter_eggs` + +Optional easter egg features. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `easter_eggs.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `easter_eggs.enabled`. | + +## `enc` + +Encryption and RSA settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `enc.d` | `` | `string` | RSA private exponent used by the encryption layer. | +| `enc.e` | `` | `string` | RSA public exponent used by the encryption layer. | +| `enc.enabled` | `1` | `boolean` | Enable RSA encryption support for the socket handshake. | +| `enc.n` | `` | `string` | RSA modulus used by the encryption layer. | + +## `essentials` + +Essentials plugin or command values. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `essentials.cmd_kill.effect.killer` | `164;182` | `list` | Semicolon-separated effect IDs used by the kill command for the killer. | +| `essentials.cmd_kill.effect.victim` | `93;89` | `list` | Semicolon-separated effect IDs used by the kill command for the victim. | + +## `flood` + +Flood-control compatibility switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `flood.with.rights` | `0` | `boolean` | Allow users with room rights to bypass the normal flood protection. | + +## `ftp` + +FTP integration settings for generated assets. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `ftp.enabled` | `0` | `boolean` | Enable FTP uploads for generated assets. | +| `ftp.host` | `example.com` | `string` | FTP host used for asset uploads. | +| `ftp.password` | `password123` | `string` | FTP password used for asset uploads. | +| `ftp.user` | `root` | `string` | FTP username used for asset uploads. | + +## `furniture` + +General furniture interaction behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `furniture.talking.range` | `2` | `integer` | Maximum tile distance at which talking furniture can react to nearby speech. | + +## `gamecenter` + +Gamecenter launchers and theme settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `gamecenter.fastfood.apiKey` | `` | `string` | API key used by the FastFood or BaseJump integration. | +| `gamecenter.fastfood.assets` | `http://localhost/swf/c_images/gamecenter_basejump/` | `url` | Asset base URL used by the FastFood or BaseJump game client. | +| `gamecenter.fastfood.background.color` | `68bbd2` | `string` | Background color used by the FastFood launcher UI. | +| `gamecenter.fastfood.enabled` | `true` | `boolean` | Enable the FastFood or BaseJump gamecenter integration. | +| `gamecenter.fastfood.text.color` | `ffffff` | `string` | Text color used by the FastFood launcher UI. | +| `gamecenter.fastfood.theme` | `default` | `string` | Theme name used by the FastFood launcher. | +| `gamecenter.snowwar.artic.bg` | `http://localhost/swf/c_images/gamecenter_snowwar/snst_bg_1_a_big.png` | `url` | Background image used for the SnowWar Arctic map. | +| `gamecenter.snowwar.assets` | `http://localhost/swf/c_images/gamecenter_snowwar/` | `url` | Asset base URL used by the SnowWar game client. | +| `gamecenter.snowwar.dragoncave.bg` | `http://localhost/swf/c_images/gamecenter_snowwar/snst_bg_2_big.png` | `url` | Background image used for the SnowWar Dragon Cave map. | +| `gamecenter.snowwar.enabled` | `true` | `boolean` | Enable the SnowWar gamecenter integration. | +| `gamecenter.snowwar.fightnight.bg` | `http://localhost/swf/c_images/gamecenter_snowwar/snst_bg_3_noscale.png` | `url` | Background image used for the SnowWar Fight Night map. | +| `gamecenter.snowwar.game.background.color` | `93d4f3` | `string` | Background color used by the SnowWar launcher UI. | +| `gamecenter.snowwar.game.start.time` | `15` | `integer` | Countdown in seconds before a SnowWar round starts. | +| `gamecenter.snowwar.game.text.color` | `000000` | `integer` | Text color used by the SnowWar launcher UI. | +| `gamecenter.snowwar.players.min` | `2` | `integer` | Minimum number of players required to start SnowWar. | +| `gamecenter.snowwar.room.id` | `0` | `boolean` | Room ID used as the SnowWar lobby or host room. | + +## `gamedata` + +Remote gamedata sources. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `gamedata.figuredata.url` | `https://habbo.com/gamedata/figuredata/0` | `url` | Remote figuredata URL used when the hotel loads avatar figure definitions. | + +## `guardians` + +Guardians and report-review settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `guardians.accept.timer` | `90` | `integer` | Time in seconds that guardians have to accept a case. | +| `guardians.maximum.guardians.total` | `10` | `integer` | Maximum number of guardians that can be assigned to one case. | +| `guardians.maximum.resends` | `2` | `integer` | Maximum number of times an unanswered guardian case can be resent. | +| `guardians.minimum.votes` | `5` | `integer` | Minimum number of guardian votes required to resolve a case. | +| `guardians.reporting.cooldown` | `900` | `integer` | Cooldown in seconds before the same user can open a new guardian report. | + +## `hotel` + +Core hotel gameplay, economy, room, catalog and moderation settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `hotel.alert.oldstyle` | `0` | `boolean` | Use the legacy generic alert window style. | +| `hotel.allow.ignore.staffs` | `1` | `boolean` | Allow users to ignore staff accounts. | +| `hotel.auto.credits.amount` | `100` | `integer` | Amount of credits granted on each automatic payout. | +| `hotel.auto.credits.enabled` | `1` | `boolean` | Enable automatic credits payouts. | +| `hotel.auto.credits.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic credits payouts for HC users. | +| `hotel.auto.credits.ignore.hotelview` | `1` | `boolean` | Skip users staying in hotel view when giving automatic credits payouts. | +| `hotel.auto.credits.ignore.idled` | `0` | `boolean` | Skip idle users when giving automatic credits payouts. | +| `hotel.auto.credits.interval` | `600` | `integer` | Interval in seconds between automatic credits payouts. | +| `hotel.auto.gotwpoints.enabled` | `0` | `boolean` | Enable automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic gotwpoints payouts for HC users. | +| `hotel.auto.gotwpoints.ignore.hotelview` | `1` | `boolean` | Skip users staying in hotel view when giving automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.ignore.idled` | `1` | `boolean` | Skip idle users when giving automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.interval` | `600` | `integer` | Interval in seconds between automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.name` | `shell` | `string` | Internal currency name used by the automatic gotwpoints payout. | +| `hotel.auto.gotwpoints.type` | `4` | `integer` | Currency type ID used by the automatic gotwpoints payout. | +| `hotel.auto.pixels.amount` | `100` | `integer` | Amount of pixels granted on each automatic payout. | +| `hotel.auto.pixels.enabled` | `1` | `boolean` | Enable automatic pixels payouts. | +| `hotel.auto.pixels.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic pixels payouts for HC users. | +| `hotel.auto.pixels.ignore.hotelview` | `1` | `boolean` | Skip users staying in hotel view when giving automatic pixels payouts. | +| `hotel.auto.pixels.ignore.idled` | `1` | `boolean` | Skip idle users when giving automatic pixels payouts. | +| `hotel.auto.pixels.interval` | `600` | `integer` | Interval in seconds between automatic pixels payouts. | +| `hotel.auto.points.amount` | `5` | `integer` | Amount of points granted on each automatic payout. | +| `hotel.auto.points.enabled` | `1` | `boolean` | Enable automatic points payouts. | +| `hotel.auto.points.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic points payouts for HC users. | +| `hotel.auto.points.ignore.hotelview` | `0` | `boolean` | Skip users staying in hotel view when giving automatic points payouts. | +| `hotel.auto.points.ignore.idled` | `0` | `boolean` | Skip idle users when giving automatic points payouts. | +| `hotel.auto.points.interval` | `600` | `integer` | Interval in seconds between automatic points payouts. | +| `hotel.banzai.points.tile.fill` | `0` | `boolean` | Configuration value used by `hotel.banzai.points.tile.fill`. | +| `hotel.banzai.points.tile.lock` | `1` | `boolean` | Configuration value used by `hotel.banzai.points.tile.lock`. | +| `hotel.banzai.points.tile.steal` | `0` | `boolean` | Configuration value used by `hotel.banzai.points.tile.steal`. | +| `hotel.bot.butler.commanddistance` | `5` | `integer` | Maximum tile distance from which a butler bot accepts commands. | +| `hotel.bot.butler.servedistance` | `5` | `integer` | Maximum tile distance from which a butler bot can serve requests. | +| `hotel.bot.chat.minimum.interval` | `5` | `integer` | Minimum number of seconds between bot chat lines. | +| `hotel.bot.max.chatdelay` | `604800` | `integer` | Maximum bot chat delay allowed when configuring scripted speech. | +| `hotel.bot.max.chatlength` | `120` | `integer` | Maximum number of characters allowed in bot chat lines. | +| `hotel.bot.max.namelength` | `15` | `integer` | Maximum number of characters allowed in bot names. | +| `hotel.bots.max.inventory` | `25` | `integer` | Maximum number of bots allowed in one inventory. | +| `hotel.bots.max.room` | `10` | `integer` | Maximum number of bots allowed in one room. | +| `hotel.calendar.default` | `test` | `string` | Default calendar campaign name or identifier. | +| `hotel.calendar.enabled` | `0` | `boolean` | Enable the hotel calendar feature. | +| `hotel.calendar.pixels.hc_modifier` | `2.0` | `number` | Multiplier applied to calendar pixel rewards for HC users. | +| `hotel.calendar.starttimestamp` | `1593561600` | `integer` | Unix timestamp used as the calendar start date. | +| `hotel.catalog.discounts.amount` | `6` | `integer` | Number of discount slots or discount batches shown by the catalog. | +| `hotel.catalog.items.display.ordernum` | `1` | `boolean` | Respect catalog item order numbers when rendering pages. | +| `hotel.catalog.ltd.limit.enabled` | `1` | `boolean` | Enable daily purchase limits for limited catalog items. | +| `hotel.catalog.purchase.cooldown` | `1` | `boolean` | Cooldown in seconds between catalog purchases. | +| `hotel.catalog.recycler.enabled` | `1` | `boolean` | Enable the catalog recycler feature. | +| `hotel.chat.max.length` | `100` | `integer` | Maximum number of characters allowed in one public chat message. | +| `hotel.daily.respect` | `3` | `integer` | Daily amount of respect points available for users. | +| `hotel.daily.respect.pets` | `3` | `integer` | Daily amount of pet respect points available for users. | +| `hotel.ecotron.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `hotel.ecotron.enabled`. | +| `hotel.ecotron.rarity.chance.1` | `1` | `boolean` | Configuration value used by `hotel.ecotron.rarity.chance.1`. | +| `hotel.ecotron.rarity.chance.2` | `4` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.2`. | +| `hotel.ecotron.rarity.chance.3` | `40` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.3`. | +| `hotel.ecotron.rarity.chance.4` | `200` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.4`. | +| `hotel.ecotron.rarity.chance.5` | `2000` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.5`. | +| `hotel.flood.mute.time` | `30` | `integer` | Mute duration in seconds applied by the hotel flood protection. | +| `hotel.floorplan.max.totalarea` | `4096` | `integer` | Maximum total floorplan area allowed for custom rooms. | +| `hotel.floorplan.max.widthlength` | `64` | `integer` | Maximum floorplan width or length allowed for custom rooms. | +| `hotel.freeze.onfreeze.loose.explosionboost` | `3` | `integer` | Number of explosion boosts lost when a player gets frozen. | +| `hotel.freeze.onfreeze.loose.snowballs` | `5` | `integer` | Number of snowballs lost when a player gets frozen. | +| `hotel.freeze.onfreeze.time.frozen` | `5` | `integer` | Time in seconds a player remains frozen. | +| `hotel.freeze.points.block` | `1` | `boolean` | Score awarded for blocking tiles in Freeze. | +| `hotel.freeze.points.effect` | `3` | `integer` | Score awarded for using Freeze effects or power-up actions. | +| `hotel.freeze.points.freeze` | `10` | `integer` | Score awarded for freezing another player in Freeze. | +| `hotel.freeze.powerup.chance` | `33` | `integer` | Chance for Freeze power-ups to spawn. | +| `hotel.freeze.powerup.max.lives` | `3` | `integer` | Maximum number of extra lives granted by a Freeze power-up. | +| `hotel.freeze.powerup.max.snowballs` | `5` | `integer` | Maximum number of extra snowballs granted by a Freeze power-up. | +| `hotel.freeze.powerup.protection.stack` | `1` | `boolean` | Allow Freeze protection power-ups to stack. | +| `hotel.freeze.powerup.protection.time` | `10` | `integer` | Protection time in seconds after receiving a Freeze protection power-up. | +| `hotel.friendcategory` | `0` | `boolean` | Default friend category ID assigned to new friends. | +| `hotel.furni.gym.achievement.olympics_c16_crosstrainer` | `CrossTrainer` | `string` | Configuration value used by `hotel.furni.gym.achievement.olympics_c16_crosstrainer`. | +| `hotel.furni.gym.achievement.olympics_c16_trampoline` | `Trampolinist` | `string` | Configuration value used by `hotel.furni.gym.achievement.olympics_c16_trampoline`. | +| `hotel.furni.gym.achievement.olympics_c16_treadmill` | `Jogger` | `string` | Configuration value used by `hotel.furni.gym.achievement.olympics_c16_treadmill`. | +| `hotel.furni.gym.forcerot.olympics_c16_crosstrainer` | `1` | `boolean` | Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_crosstrainer`. | +| `hotel.furni.gym.forcerot.olympics_c16_trampoline` | `0` | `boolean` | Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_trampoline`. | +| `hotel.furni.gym.forcerot.olympics_c16_treadmill` | `1` | `boolean` | Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_treadmill`. | +| `hotel.gifts.box_types` | `0,1,2,3,4,5,6,8` | `list` | Comma-separated list of gift box type IDs allowed in the catalog. | +| `hotel.gifts.length.max` | `300` | `integer` | Maximum message length allowed on gift notes. | +| `hotel.gifts.ribbon_types` | `0,1,2,3,4,5,6,7,8,9,10` | `list` | Comma-separated list of ribbon type IDs allowed in the catalog. | +| `hotel.gifts.special.price` | `10` | `integer` | Credit price used by special gift boxes. | +| `hotel.home.room` | `0` | `boolean` | Room ID used as the default home room for new users. | +| `hotel.inventory.max.items` | `7500` | `integer` | Maximum number of items allowed in one inventory. | +| `hotel.item.trap.hween14_rare2` | `3000` | `integer` | Configuration value used by `hotel.item.trap.hween14_rare2`. | +| `hotel.item.trap.hween_c17_handstrap` | `3000` | `integer` | Configuration value used by `hotel.item.trap.hween_c17_handstrap`. | +| `hotel.item.trap.hween_c17_spiketrap` | `3000` | `integer` | Configuration value used by `hotel.item.trap.hween_c17_spiketrap`. | +| `hotel.item.trap.pirate_sandtrap` | `3000` | `integer` | Configuration value used by `hotel.item.trap.pirate_sandtrap`. | +| `hotel.jukebox.limit.large` | `20` | `integer` | Track limit used by large jukebox furniture. | +| `hotel.jukebox.limit.normal` | `10` | `integer` | Track limit used by normal jukebox furniture. | +| `hotel.log.chat` | `1` | `boolean` | Enable logging for chat. | +| `hotel.log.chat.private` | `1` | `boolean` | Enable logging for chat private. | +| `hotel.log.room.enter` | `1` | `boolean` | Enable logging for room enter. | +| `hotel.log.trades` | `1` | `boolean` | Enable logging for trades. | +| `hotel.marketplace.currency` | `0` | `boolean` | Currency type used for marketplace prices and taxes. | +| `hotel.marketplace.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `hotel.marketplace.enabled`. | +| `hotel.max.bots.room` | `10` | `integer` | Maximum number of bots allowed in one room. | +| `hotel.max.duckets` | `9000000` | `integer` | Maximum amount of duckets a user can hold. | +| `hotel.messenger.offline.messaging.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `hotel.messenger.offline.messaging.enabled`. | +| `hotel.messenger.search.maxresults` | `50` | `integer` | Maximum number of results returned by messenger user searches. | +| `hotel.name` | `Habbo Hotel` | `string` | Public hotel name shown across the client and outgoing messages. | +| `hotel.navigator.camera` | `1` | `boolean` | Enable navigator room previews or camera mode. | +| `hotel.navigator.owner` | `HabboHotel` | `string` | Default owner name displayed by the navigator. | +| `hotel.navigator.popular.amount` | `35` | `integer` | Number of rooms shown in the popular rooms list. | +| `hotel.navigator.popular.category.maxresults` | `10` | `integer` | Maximum number of rooms shown per popular category. | +| `hotel.navigator.popular.listtype` | `1` | `boolean` | List type used for the popular rooms tab. | +| `hotel.navigator.populartab.publics` | `0` | `boolean` | Include public rooms inside the popular rooms tab. | +| `hotel.navigator.search.maxresults` | `35` | `integer` | Maximum number of results returned by navigator searches. | +| `hotel.navigator.sort.ordernum` | `1` | `boolean` | Respect order numbers when sorting navigator results. | +| `hotel.navigator.staffpicks.categoryid` | `1` | `boolean` | Category ID used for the staff picks tab. | +| `hotel.nux.gifts.enabled` | `0` | `boolean` | Enable the NUX gift flow for new users. | +| `hotel.pets.max.inventory` | `25` | `integer` | Maximum number of pets allowed in one inventory. | +| `hotel.pets.max.room` | `10` | `integer` | Maximum number of pets allowed in one room. | +| `hotel.pets.name.length.max` | `15` | `integer` | Maximum pet name length. | +| `hotel.pets.name.length.min` | `3` | `integer` | Minimum pet name length. | +| `hotel.player.name` | `Habbo` | `string` | Generic player label used by text templates and client messages. | +| `hotel.purchase.ltd.limit.daily.item` | `3` | `integer` | Maximum number of the same limited item a user can buy per day. | +| `hotel.purchase.ltd.limit.daily.total` | `10` | `integer` | Maximum number of limited items a user can buy per day across all limited sales. | +| `hotel.refill.daily` | `86400` | `integer` | Cooldown in seconds before daily counters such as respect are refilled. | +| `hotel.rollers.speed.maximum` | `100` | `integer` | Maximum roller delay or speed value accepted by roller furniture. | +| `hotel.room.enter.logs` | `1` | `boolean` | Enable room-entry logs. | +| `hotel.room.floorplan.check.enabled` | `1` | `boolean` | Validate custom floorplans before rooms are saved. | +| `hotel.room.furni.max` | `2500` | `integer` | Maximum amount of furniture allowed in one room. | +| `hotel.room.nooblobby` | `3` | `integer` | Room ID used as the newbie lobby. | +| `hotel.room.public.doortile.kick` | `0` | `boolean` | Kick users who stand on public room door tiles. | +| `hotel.room.rollers.norules` | `0` | `boolean` | Allow rollers to ignore normal placement rules. | +| `hotel.room.rollers.roll_avatars.max` | `1` | `boolean` | Maximum number of avatars that rollers can move at once. | +| `hotel.room.stickies.max` | `200` | `integer` | Maximum number of sticky notes allowed in one room. | +| `hotel.room.stickypole.prefix` | `%timestamp%, %username%:\\r` | `template` | Prefix template written by sticky pole furniture. | +| `hotel.room.tags.staff` | `staff;official;habbo` | `list` | Semicolon-separated staff room tags. | +| `hotel.rooms.auto.idle` | `1` | `boolean` | Allow empty rooms to switch into the idle state automatically. | +| `hotel.rooms.deco_hosting` | `1` | `boolean` | Enable decoration-hosting features for rooms. | +| `hotel.rooms.handitem.time` | `100` | `integer` | Time in seconds before temporary hand items are cleared. | +| `hotel.rooms.max.favorite` | `30` | `integer` | Maximum number of favorite rooms allowed per user. | +| `hotel.roomuser.idle.cycles` | `300` | `integer` | Idle cycle count before a room user is marked idle. | +| `hotel.roomuser.idle.cycles.kick` | `900` | `integer` | Idle cycle count before a room user is kicked for idling. | +| `hotel.roomuser.idle.not_dancing.ignore.wired_idle` | `0` | `boolean` | Ignore the wired idle status when checking the room idle rule. | +| `hotel.sanctions.enabled` | `1` | `boolean` | Enable the sanctions system. | +| `hotel.shop.discount.modifier` | `6` | `integer` | Modifier used by the shop discount calculation. | +| `hotel.talenttrack.enabled` | `1` | `boolean` | Enable the talent track feature. | +| `hotel.targetoffer.id` | `1` | `boolean` | Offer ID requested when the client asks for a targeted offer. | +| `hotel.teleport.locked.allowed` | `1` | `boolean` | Allow users to use teleports inside locked rooms when they otherwise qualify. | +| `hotel.trading.enabled` | `1` | `boolean` | Enable room trading. | +| `hotel.trading.requires.perk` | `0` | `boolean` | Require the trading perk before users may trade. | +| `hotel.trophies.length.max` | `300` | `integer` | Maximum value used by `hotel.trophies.length.max`. | +| `hotel.users.clothingvalidation.onchangelooks` | `0` | `boolean` | Run clothing validation when the related action occurs: onchangelooks. | +| `hotel.users.clothingvalidation.onfballgate` | `0` | `boolean` | Run clothing validation when the related action occurs: onfballgate. | +| `hotel.users.clothingvalidation.onhcexpired` | `0` | `boolean` | Run clothing validation when the related action occurs: onhcexpired. | +| `hotel.users.clothingvalidation.onlogin` | `0` | `boolean` | Run clothing validation when the related action occurs: onlogin. | +| `hotel.users.clothingvalidation.onmannequin` | `0` | `boolean` | Run clothing validation when the related action occurs: onmannequin. | +| `hotel.users.clothingvalidation.onmimic` | `0` | `boolean` | Run clothing validation when the related action occurs: onmimic. | +| `hotel.users.max.friends` | `300` | `integer` | Maximum number of friends allowed for normal users. | +| `hotel.users.max.friends.hc` | `1100` | `integer` | Maximum number of friends allowed for HC users. | +| `hotel.users.max.rooms` | `50` | `integer` | Maximum number of rooms allowed for normal users. | +| `hotel.users.max.rooms.hc` | `75` | `integer` | Maximum number of rooms allowed for HC users. | +| `hotel.view.ltdcountdown.enabled` | `1` | `boolean` | Enable the limited-countdown hotel-view widget. | +| `hotel.view.ltdcountdown.itemid` | `10388` | `integer` | Item ID shown by the limited-countdown widget. | +| `hotel.view.ltdcountdown.itemname` | `trophy_netsafety_0` | `string` | Item name shown by the limited-countdown widget. | +| `hotel.view.ltdcountdown.pageid` | `13` | `integer` | Catalog page ID linked by the limited-countdown widget. | +| `hotel.view.ltdcountdown.timestamp` | `1519496132` | `integer` | Unix timestamp used by the limited-countdown widget. | +| `hotel.welcome.alert.delay` | `10000` | `integer` | Delay in milliseconds before the welcome alert is shown. | +| `hotel.welcome.alert.enabled` | `0` | `boolean` | Enable the welcome alert shown after login. | +| `hotel.welcome.alert.message` | `Welcome to Habbo Hotel %user%!` | `template` | Message template used by the welcome alert. | +| `hotel.welcome.alert.oldstyle` | `0` | `boolean` | Use the legacy welcome alert window style. | +| `hotel.wordfilter.automute` | `1` | `boolean` | Mute duration in minutes applied when word-filter automute is triggered. | +| `hotel.wordfilter.enabled` | `1` | `boolean` | Enable the word filter system. | +| `hotel.wordfilter.messenger` | `1` | `boolean` | Apply the word filter to messenger messages. | +| `hotel.wordfilter.normalise` | `1` | `boolean` | Normalise text before checking it against the word filter. | +| `hotel.wordfilter.replacement` | `bobba` | `string` | Replacement word used when text is censored. | +| `hotel.wordfilter.rooms` | `1` | `boolean` | Apply the word filter to room chat. | + +## `hotelview` + +Hotel-view widgets and promotional data. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `hotelview.halloffame.query` | `SELECT users.look, users.username, users.id, users_settings.hof_points FROM users_settings INNER JOIN users ON users_settings.user_id = users.id WHERE hof_points > 0 ORDER BY hof_points DESC, users.id ASC LIMIT 10` | `sql` | SQL query used to populate the hotel-view hall of fame panel. | +| `hotelview.promotional.points` | `100` | `integer` | Amount of activity points awarded by the hotel-view promotion. | +| `hotelview.promotional.points.type` | `5` | `integer` | Activity point type used by the hotel-view promotional reward. | +| `hotelview.promotional.reward.id` | `11043` | `integer` | Base item ID used by the hotel-view promotional reward. | +| `hotelview.promotional.reward.name` | `bonusbag20_2` | `string` | Public item name used by the hotel-view promotional reward. | + +## `imager` + +Internal image generator paths and URLs. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `imager.internal.enabled` | `1` | `boolean` | Generate images locally instead of relying on an external imager service. | +| `imager.location.badgeparts` | `/var/www/testhotel/Cosmic/public/usercontent/badgeparts` | `string` | Filesystem path where badge part assets are stored. | +| `imager.location.output.badges` | `/var/www/testhotel/Cosmic/public/usercontent/badgeparts/generated/` | `string` | Filesystem output path for generated badges. | +| `imager.location.output.camera` | `/var/www/testhotel/Cosmic/public/usercontent/camera/` | `string` | Filesystem output path for saved camera photos. | +| `imager.location.output.thumbnail` | `/var/www/testhotel/Cosmic/public/usercontent/camera/thumbnail/` | `string` | Filesystem output path for generated camera thumbnails. | +| `imager.url.youtube` | `imager.php?url=http://img.youtube.com/vi/%video%/default.jpg` | `template` | Template URL used to fetch YouTube thumbnails. | + +## `images` + +Static client image path helpers. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `images.gamecenter.basejump` | `c_images/gamecenter_basejump/` | `string` | Client asset path used for the basejump gamecenter images. | +| `images.gamecenter.snowwar` | `c_images/gamecenter_snowwar/` | `string` | Client asset path used for the snowwar gamecenter images. | + +## `info` + +Global information panel toggle. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `info.shown` | `1` | `boolean` | Show the hotel information panel or startup information message. | + +## `invisible` + +Invisible-mode behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `invisible.prevent.chat` | `0` | `boolean` | Prevent invisible users from speaking in rooms. | + +## `io` + +Socket and Netty threading settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `io.bossgroup.threads` | `1` | `boolean` | Number of Netty boss-group threads used by the socket server. | +| `io.client.multithreaded.handler` | `1` | `boolean` | Handle incoming client packets with a multi-threaded pipeline. | +| `io.workergroup.threads` | `5` | `integer` | Number of Netty worker-group threads used by the socket server. | + +## `logging` + +Structured emulator logging toggles. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `logging.debug` | `0` | `boolean` | Enable extra debug logging in the emulator logger. | +| `logging.errors.packets` | `0` | `boolean` | Log packet parsing errors. | +| `logging.errors.runtime` | `1` | `boolean` | Log runtime exceptions. | +| `logging.errors.sql` | `1` | `boolean` | Log SQL errors. | +| `logging.packets` | `0` | `boolean` | Log packet traffic in the standard logger. | +| `logging.packets.undefined` | `0` | `boolean` | Log undefined packets in the standard logger. | + +## `marketplace` + +Marketplace compatibility flag. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `marketplace.enabled` | `1` | `boolean` | Global switch for the marketplace subsystem. | + +## `monsterplant` + +Monster plant seed item mapping. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `monsterplant.seed.item_id` | `4582` | `integer` | Configuration value used by `monsterplant.seed.item_id`. | +| `monsterplant.seed_rare.item_id` | `4604` | `integer` | Configuration value used by `monsterplant.seed_rare.item_id`. | + +## `moodlight` + +Moodlight validation switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `moodlight.color_check.enabled` | `1` | `boolean` | Validate moodlight color values before applying them. | + +## `navigator` + +Navigator static definitions. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `navigator.eventcategories` | `1,Hottest Events,false;2,Parties & Music,true;3,Role Play,true;4,Help Desk,true;5,Trading,true;6,Games,true;7,Debates & Discussions,true;8,Grand Openings,true;9,Friending,true;10,Jobs,true;11,Group Events,true` | `list` | Semicolon-separated navigator event category definitions shown in the events tab. | + +## `networking` + +Low-level networking compatibility switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `networking.tcp.proxy` | `0` | `boolean` | Enable TCP proxy-aware networking behaviour. | + +## `notify` + +Server-side notification automation. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `notify.staff.chat.auto.report` | `1` | `boolean` | Automatically notify staff when a chat report is created. | + +## `path` + +Asset path helpers. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `path.furniture.icons` | `${image.library.url}/icons/` | `template` | Base path used by the client to load furniture icon assets. | + +## `pathfinder` + +Pathfinder safety and performance settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `pathfinder.execution_time.milli` | `25` | `integer` | Maximum pathfinder execution time in milliseconds before aborting. | +| `pathfinder.max_execution_time.enabled` | `1` | `boolean` | Enforce the pathfinder execution time limit. | +| `pathfinder.step.allow.falling` | `1` | `boolean` | Allow the pathfinder to walk down falling steps. | +| `pathfinder.step.maximum.height` | `1.1` | `number` | Maximum height difference the pathfinder may step onto. | + +## `pirate_parrot` + +Pirate parrot text and bubble behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `pirate_parrot.message.bubble` | `28` | `integer` | Chat bubble style ID used by the pirate parrot. | +| `pirate_parrot.message.count` | `6` | `integer` | Number of predefined messages available to the pirate parrot. | + +## `postit` + +Post-it constraints. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `postit.charlimit` | `366` | `integer` | Maximum number of characters allowed on post-it notes. | + +## `pyramids` + +Pyramids minigame timing. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `pyramids.max.delay` | `18` | `integer` | Maximum delay allowed in the Pyramids minigame or puzzle timing. | + +## `retro` + +Retro compatibility switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `retro.style.homeroom` | `1` | `boolean` | Use retro-style home room behaviour in the navigator or onboarding flow. | + +## `room` + +Generic room chat and promotion behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `room.chat.delay` | `0` | `boolean` | Extra room chat delay applied before users can speak again. | +| `room.chat.mutearea.allow_whisper` | `1` | `boolean` | Allow whispering while a user stands inside a mute area. | +| `room.chat.prefix.format` | `[%prefix%] ` | `string` | HTML or text format used for room chat prefixes. | +| `room.promotion.badge` | `RADZZ` | `string` | Badge code displayed on promoted rooms. | + +## `rosie` + +Rosie-related client notifications and purchase currency. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `rosie.bubble.image.url` | `${image.library.url}notifications/generic.png` | `template` | Image used by Rosie bubble notifications. | +| `rosie.buyroom.currency.type` | `5` | `integer` | Currency type used by Rosie when buying a room or room package. | + +## `runtime` + +Executor and thread sizing. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `runtime.threads` | `8` | `integer` | Configuration value used by `runtime.threads`. | + +## `save` + +Chat persistence toggles. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `save.private.chats` | `1` | `boolean` | Configuration value used by `save.private.chats`. | +| `save.room.chats` | `1` | `boolean` | Configuration value used by `save.room.chats`. | + +## `scripter` + +Scripter or modtool integration. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `scripter.modtool.tickets` | `1` | `boolean` | Expose moderation tickets to the scripter or automation tooling. | + +## `seasonal` + +Seasonal currency mapping. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `seasonal.currency.diamond` | `5` | `integer` | Currency type ID used for diamonds. | +| `seasonal.currency.ducket` | `0` | `boolean` | Currency type ID used for duckets. | +| `seasonal.currency.names` | `ducket;pixel;shell;diamond` | `list` | Semicolon-separated display names for seasonal currency types. | +| `seasonal.currency.pixel` | `0` | `boolean` | Currency type ID used for pixels. | +| `seasonal.currency.shell` | `4` | `integer` | Currency type ID used for shells. | +| `seasonal.primary.type` | `5` | `integer` | Primary seasonal currency type ID. | +| `seasonal.types` | `0;1;2;3;4;5;101;102;103;104;105` | `list` | Semicolon-separated list of currency type IDs treated as seasonal currencies. | + +## `subscriptions` + +HC scheduler, payday and discount configuration. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `subscriptions.hc.achievement` | `VipHC` | `string` | Achievement code granted for the HC subscription tier. | +| `subscriptions.hc.discount.days_before_end` | `7` | `integer` | Number of days before expiry when HC discount offers become available. | +| `subscriptions.hc.discount.enabled` | `1` | `boolean` | Enable discounted HC renewal offers. | +| `subscriptions.hc.payday.creditsspent_reset_on_expire` | `1` | `boolean` | Reset tracked credits spent when the HC subscription expires. | +| `subscriptions.hc.payday.currency` | `credits` | `string` | Currency rewarded by the HC payday system. | +| `subscriptions.hc.payday.enabled` | `1` | `boolean` | Enable the HC payday reward system. | +| `subscriptions.hc.payday.interval` | `1 month` | `string` | Date interval used between HC payday reward runs. | +| `subscriptions.hc.payday.next_date` | `2020-10-15 00:00:00` | `string` | Next scheduled execution date for HC payday rewards. | +| `subscriptions.hc.payday.percentage` | `10` | `integer` | Percentage of eligible spending returned by HC payday. | +| `subscriptions.hc.payday.streak` | `7=5;30=10;60=15;90=20;180=25;365=30` | `list` | Semicolon-separated streak thresholds and rewards for HC payday. | +| `subscriptions.scheduler.enabled` | `1` | `boolean` | Enable the subscription background scheduler. | +| `subscriptions.scheduler.interval` | `10` | `integer` | Interval in minutes between subscription scheduler runs. | + +## `team` + +Compatibility markers for team or wired integrations. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `team.wired.update.rc-1` | `DO NOT REMOVE THIS SETTING!` | `string` | Compatibility marker used by the custom team wired implementation. Do not remove. | + +## `youtube` + +YouTube integration credentials. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `youtube.apikey` | `` | `string` | API key used by the YouTube integration. | + diff --git a/docs/permissions_schema_reference.md b/docs/permissions_schema_reference.md new file mode 100644 index 00000000..58fabd33 --- /dev/null +++ b/docs/permissions_schema_reference.md @@ -0,0 +1,187 @@ +# Permissions schema reference + +## Overview + +The legacy `permissions` table stores: + +- one row per rank +- one column per permission key + +That works for runtime, but it becomes very hard to read and maintain once the number of permission keys grows. + +The updated design keeps only the rank metadata separated, while the permission matrix itself becomes one readable table: + +- `permission_ranks` + - one row per rank + - stores rank metadata such as `rank_name`, `badge`, `level`, `prefix`, `room_effect`, and the automatic currency amounts +- `permission_definitions` + - one row per permission key + - stores the permission comment in the same row + - stores one column per rank using the format `rank_` + +Example: + +| permission_key | max_value | comment | rank_1 | rank_2 | rank_7 | +| --- | --- | --- | --- | --- | --- | +| `acc_ads_background` | `1` | Allows editing room advertisement backgrounds. | `0` | `0` | `1` | + +## Runtime behavior + +- The emulator still supports the legacy `permissions` table as a fallback. +- If `permission_ranks` and `permission_definitions` exist and contain data, the emulator loads the new schema instead. +- If the new schema is missing, incomplete, or fails to load, the emulator falls back to the legacy `permissions` table automatically. + +Relevant runtime files: + +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java:45` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java:71` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java:57` + +## Tables + +### `permission_ranks` + +This table stores only rank metadata: + +- `id` +- `rank_name` +- `hidden_rank` +- `badge` +- `job_description` +- `staff_color` +- `staff_background` +- `level` +- `room_effect` +- `log_commands` +- `prefix` +- `prefix_color` +- `auto_credits_amount` +- `auto_pixels_amount` +- `auto_gotw_amount` +- `auto_points_amount` + +#### `permission_ranks` field meanings + +- `id` + - Numeric rank id used everywhere else in the emulator, including `users.rank` and the dynamic `rank_` columns in `permission_definitions`. +- `rank_name` + - Human-readable name of the rank, such as `User`, `Moderator`, or `Administrator`. +- `hidden_rank` + - When enabled, the rank is treated as hidden in places where staff visibility should be reduced. +- `badge` + - Badge code automatically associated with the rank. +- `job_description` + - Staff/job description text shown in features that expose rank profile details. +- `staff_color` + - Hex color used by staff UI or visuals that depend on the rank color. +- `staff_background` + - Background asset name used for staff visuals tied to the rank. +- `level` + - Priority/order value of the rank; higher values usually mean stronger staff level or broader access. +- `room_effect` + - Default avatar effect id associated with the rank when that feature is used. +- `log_commands` + - Controls whether commands executed by users with this rank should be logged in command logs. +- `prefix` + - Short in-room staff prefix/tag associated with the rank. +- `prefix_color` + - Hex color used for the displayed rank prefix. +- `auto_credits_amount` + - Automatic credit amount granted by rank-based reward/payday style logic, if used by the hotel. +- `auto_pixels_amount` + - Automatic duckets/pixels amount granted by rank-based reward/payday style logic, if used by the hotel. +- `auto_gotw_amount` + - Automatic GOTW-style points amount granted by rank-based reward/payday style logic, if used by the hotel. +- `auto_points_amount` + - Automatic activity-points amount granted by rank-based reward/payday style logic, if used by the hotel. + +### `permission_definitions` + +This table stores: + +- `permission_key` +- `max_value` +- `comment` +- one dynamic column per rank: + - `rank_1` + - `rank_2` + - `rank_3` + - ... + +That means the table itself is already the readable matrix you wanted: + +- rows = permission keys +- columns = rank values +- comment stays next to the key + +## Value semantics + +Permission values keep the same meaning as today: + +- `0` = disabled +- `1` = allowed +- `2` = allowed only when room-owner rights may be used + +The schema stores that information in: + +- `permission_definitions.max_value` + +## Migration behavior + +`Database Updates/004_normalize_permissions_schema.sql` does the following: + +1. keeps the legacy `permissions` table untouched +2. creates `permission_ranks` +3. creates `permission_definitions` +4. copies rank metadata from `permissions` into `permission_ranks` +5. creates any missing `rank_` columns in `permission_definitions` +6. creates one row per permission key with `max_value` and a comment +7. applies curated per-key comments so every permission explains what it actually does in code +8. copies each old permission value into the proper `rank_` column + +It also removes the older experimental objects if they already exist: + +- `permission_rank_values` +- `permission_nodes` +- `permissions_matrix_view` +- `refresh_permissions_matrix_view` + +## Adding a new rank later + +When you add a new rank after the migration: + +1. insert the rank metadata into `permission_ranks` +2. reload permissions with emulator restart or `:update_permissions` +3. the emulator automatically creates the missing `rank_` column in `permission_definitions` if it does not exist yet +4. set the new `rank_` values in `permission_definitions` + +You can still run the helper procedure manually if you want to sync the schema before the next reload: + +```sql +CALL refresh_permission_definition_rank_columns(); +``` + +If you want to refresh all values again from the old legacy table during rollout, you can also run: + +```sql +CALL refresh_permission_definition_values(); +``` + +## Notes about comments and legacy keys + +The comments stored in `permission_definitions.comment` are intentionally hand-curated. + +- Where a Java handler exists, the comment follows the real runtime behavior. +- Where only legacy command texts exist, the comment marks the key as legacy and explains the intended behavior from those texts. +- Where a key is still present for compatibility but no direct handler is found in the current tree, the comment says so explicitly. + +The new schema intentionally preserves legacy and inconsistent permission keys so current functionality stays intact. + +Examples: + +- `cmd_word_quiz` +- `cmd_wordquiz` +- `cms_dance` +- `kiss_cmd` + +Those can be cleaned up later only after runtime behavior has been verified and the hotel no longer depends on the old names. diff --git a/docs/wired_bug_audit.md b/docs/wired_bug_audit.md new file mode 100644 index 00000000..9ad5f765 --- /dev/null +++ b/docs/wired_bug_audit.md @@ -0,0 +1,382 @@ +# Wired Bug Audit + +## 1. Scopo + +Questo documento raccoglie i **potenziali bug**, le **aree fragili** e le **incoerenze architetturali** emerse durante l’analisi del sistema wired. + +Non tutti i punti qui sotto sono bug già riprodotti al 100%, ma sono: + +- problemi già visti in comportamento reale +- incongruenze tra runtime e UI +- zone del codice che possono generare regressioni o risultati non deterministici + +Riferimenti principali: + +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired` + +--- + +## 2. Sintesi Priorità + +| Priorità | Tema | Stato | +|---|---|---| +| Alta | `context` variabili esposto ma non implementato davvero | Incoerenza forte | +| Alta | Doppio runtime (`WiredManager` vs `WiredHandler`) | Rischio architetturale | +| Alta | Ordine effect non sempre garantito senza extra esplicito | Rischio comportamentale | +| Alta | Path movimento legacy può ancora far trapelare update intermedi | Già osservato in stanza | +| Media | Tick a `50ms` ma delay wired in step da `500ms` | Semantica non uniforme | +| Media | Polling realtime `:wired` a `50ms` | Rischio carico/runtime noise | +| Media | `click furni` ora immediato, queue/cancel svuotati | Possibile regressione | +| Media | Semantica timestamp variabili non uniforme tra target types | Possibile confusione logica | + +--- + +## 3. Audit Dettagliato + +## 3.1 `context` nelle variabili: esposto ma non veramente supportato + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** effetti/condizioni/extras variabili + +### Problema + +Nel layout e in parte della serializzazione compare il target `context`, ma in più punti il runtime lo rifiuta esplicitamente oppure restituisce direttamente `false`. + +Questo crea una situazione pericolosa: + +- il designer pensa che la feature esista +- il box si salva o si configura parzialmente +- ma poi in esecuzione non produce il comportamento atteso + +### Evidenze + +- `WiredEffectGiveVariable.java:197` + - il save rifiuta `TARGET_CONTEXT` +- `WiredConditionVariableValueMatch.java:181` + - `case TARGET_CONTEXT -> false` +- `WiredConditionVariableAgeMatch.java:146` + - `case TARGET_CONTEXT -> false` +- `WiredExtraTextOutputVariable.java:83` + - il save rifiuta `TARGET_CONTEXT` + +### Impatto pratico + +- stack che sembrano validi in UI ma non funzionano a runtime +- falsi negativi nelle condition variabili +- placeholder testuali variabili non disponibili quando l’utente si aspetta il target context + +### Fix suggerito + +Scegliere una direzione netta: + +1. **o** implementare davvero `context` in tutti i flow variabili +2. **o** rimuoverlo completamente da UI, save e runtime finché non è pronto + +La seconda opzione è la più sicura nel breve periodo. + +--- + +## 3.2 Doppio runtime wired ancora presente + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** architettura core + +### Problema + +`WiredManager` dichiara di essere il runtime esclusivo e tratta i vecchi flag come sola compatibilità, ma `WiredHandler` esiste ancora con entrypoint completi e logica propria. + +### Evidenze + +- `WiredManager.java:136` + - warning esplicito: `wired.engine.enabled / wired.engine.exclusive are now compatibility-only flags` +- `WiredManager.java:174` + - `isEnabled()` dipende solo dall’inizializzazione del manager +- `WiredManager.java:182` + - `isExclusive()` ritorna sempre `true` +- `WiredHandler.java:63` + - entrypoint legacy completo `handle(...)` +- `WiredHandler.java:114` + - supporto separato per `handleCustomTrigger(...)` + +### Impatto pratico + +Se qualunque pezzo di codice, plugin o path legacy entra ancora in `WiredHandler`, si possono avere: + +- ordine effect diverso +- scheduling delay diverso +- condition flow diverso +- diagnostica/monitor non coerente col nuovo engine + +### Fix suggerito + +- definire un solo entrypoint runtime ufficiale +- se `WiredHandler` deve restare, trasformarlo in adapter minimo che inoltra sempre al nuovo engine +- aggiungere log o metriche per rilevare qualsiasi ingresso nel path legacy + +--- + +## 3.3 Ordine degli effect non sempre deterministico senza `wf_xtra_exec_in_order` + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** esecuzione stack + +### Problema + +Nel path legacy, l’ordinamento stabile viene applicato chiaramente solo in presenza di `wf_xtra_exec_in_order` oppure in casi specifici (`unseen`). + +Negli altri casi, l’ordine si appoggia alla collezione che arriva dal runtime. + +### Evidenze + +- `WiredHandler.java:224` + - rileva `hasExtraExecuteInOrder` +- `WiredHandler.java:230` + - ordina con `WiredExecutionOrderUtil.sort(effects)` solo in alcuni casi +- `WiredHandler.java:249` + - usa direttamente `effectList` in ordered mode + +### Impatto pratico + +Stack come: + +- `move_rotate` + `match_to_sshot` +- `toggle` + `reset` +- `give_var` + `change_var_val` + +possono produrre risultati diversi se si assume implicitamente un ordine che il runtime non promette davvero. + +### Fix suggerito + +- decidere se l’ordine stack deve essere sempre stabile di default +- in alternativa, mantenere la regola attuale ma documentarla in modo molto esplicito +- se si lascia la regola attuale, conviene segnalare in UI che l’ordine è garantito solo con `wf_xtra_exec_in_order` + +--- + +## 3.4 Il path movimento legacy può ancora far vedere movimenti intermedi + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** movement pipeline + +### Problema + +Il helper legacy di movimento usa ancora un fallback che, se il collector non è attivo, invia subito `FloorItemOnRollerComposer`. + +Questo può far trapelare al client uno stato intermedio che in teoria avrebbe dovuto essere nascosto da batching o restore finale. + +### Evidenze + +- `WiredMoveCarryHelper.java:163` + - metodo `moveFurniLegacy(...)` +- `WiredMoveCarryHelper.java:179` + - usa il collector se disponibile +- `WiredMoveCarryHelper.java:196` + - fallback diretto a `FloorItemOnRollerComposer` + +### Impatto pratico + +È coerente con il tipo di bug già visto: + +- oggetto che “si vede muovere” +- poi viene riportato nello stato corretto +- ma il client ha già ricevuto un update intermedio + +### Fix suggerito + +- evitare qualsiasi composer diretto nel path legacy quando la logica wired moderna è attiva +- centralizzare tutti i movement update in un unico collector finale +- aggiungere test specifici per: + - `move_rotate` + `match_to_sshot` + - stacked move effects nello stesso tick + +--- + +## 3.5 Tick a `50ms`, ma delay wired ancora a step da `500ms` + +- **Gravità:** Media +- **Confidenza:** Alta +- **Area:** semantica temporale + +### Problema + +Il sistema oggi ha due granularità temporali diverse: + +- repeaters / tickables a `50ms` +- delay wired classico a `delay * 500ms` + +### Evidenze + +- `WiredTickService.java:48` + - `DEFAULT_TICK_INTERVAL_MS = 50` +- `WiredTickService.java:175` + - `scheduleAtFixedRate(...)` +- `WiredEngine.java:753` + - `long delayMs = delay * 500L` +- `WiredHandler.java:369` + - stesso schema `delay * 500L` + +### Impatto pratico + +Non è per forza un bug, ma può creare: + +- aspettative sbagliate nel builder dei wired +- sensazione di desync tra repeater e delay +- stack “velocissimi” su tick ma “grossolani” sugli effect ritardati + +### Fix suggerito + +- o si accetta questa doppia semantica e la si documenta ovunque +- o si introduce una nuova famiglia di delay high-resolution separata dal delay classico + +--- + +## 3.6 `:wired` realtime a `50ms` può diventare rumoroso/pesante + +- **Gravità:** Media +- **Confidenza:** Alta +- **Area:** tooling monitor/inspection + +### Problema + +Le request di monitor e variabili ora sono rate-limitate a `50ms`. + +### Evidenze + +- `WiredMonitorRequestEvent.java:39` + - `return 50` +- `WiredUserVariablesRequestEvent.java:20` + - `return 50` + +### Impatto pratico + +Su una stanza attiva o con più client staff aperti: + +- carico rete maggiore +- più rumore sul server +- rischio di mascherare problemi reali con spam di refresh + +### Fix suggerito + +- spostare dove possibile a push/event driven +- lasciare `50ms` solo per il minimo indispensabile +- differenziare: + - monitor heavy/debug + - inspection live + - variables snapshot + +--- + +## 3.7 `click furni` ora è immediato: queue/cancel svuotati + +- **Gravità:** Media +- **Confidenza:** Alta +- **Area:** eventi click furni + +### Problema + +La queue dei click furni è stata semplificata: ora il click parte subito, e il cancel path è vuoto. + +### Evidenze + +- `WiredManager.java:274` + - `queueUserClicksFurni(...)` chiama subito `triggerUserClicksFurni(...)` +- `WiredManager.java:282` + - `cancelPendingUserClicksFurni(...)` non fa nulla + +### Impatto pratico + +Se qualche comportamento vecchio dipendeva da: + +- debounce +- cancel +- click differito + +ora può cambiare senza che il mapping sia ovvio. + +### Fix suggerito + +- decidere se il comportamento immediato è quello definitivo +- se sì, documentarlo come breaking behavior +- se no, reintrodurre una queue reale con semantica esplicita + +--- + +## 3.8 Semantica timestamp variabili non uniforme tra target type + +- **Gravità:** Media +- **Confidenza:** Media +- **Area:** sistema variabili + +### Problema + +Le variabili utente e furni hanno senso come “assegnazione con creation/update time”, mentre le room/global variables hanno soprattutto senso sul solo `update time`. + +Questo può diventare ambiguo quando si usano: + +- `wf_cnd_var_age_match` +- sorting per creation/update +- UI manage/inspection + +### Evidenze + +- `WiredConditionVariableAgeMatch.java` + - il target room/global vive soprattutto come valore di update +- le scelte di prodotto già fatte in `:wired` vanno in questa direzione + +### Impatto pratico + +- il builder può pensare che “tempo di creazione” sulle global sia forte quanto sulle user/furni +- condition o sort possono essere semanticamente strani anche se “funzionano” + +### Fix suggerito + +- trattare esplicitamente `room/global` come `updated-only` +- disabilitare in UI le opzioni che non hanno senso forte +- o documentare in modo molto chiaro la differenza + +--- + +## 4. Backlog Consigliato + +Ordine suggerito di intervento: + +1. **Chiudere il target `context`** + - o implementarlo davvero + - o toglierlo da UI/save/runtime +2. **Unificare il runtime** + - lasciare un solo entrypoint ufficiale +3. **Stabilire la regola sull’ordine effect** + - default stabile o ordine esplicito con extra +4. **Chiudere il leak dei movement update legacy** + - niente composer fuori collector quando wired moderno è attivo +5. **Ripensare il realtime di `:wired`** + - spostare il più possibile da polling a push + +--- + +## 5. Nota Finale + +Il sistema wired attuale è già molto più potente del modello classico, soprattutto per: + +- variabili +- signal routing +- selectors avanzati +- monitor +- manage/inspection + +Proprio per questo, le zone fragili oggi non sono tanto i box semplici, ma: + +- la coesistenza di due runtime +- la semantica temporale +- i movement stack +- le feature variabili ancora “mezze esposte” + +Questi sono i punti che più probabilmente spiegano i bug strani o intermittenti. diff --git a/docs/wired_full_reference.html b/docs/wired_full_reference.html new file mode 100644 index 00000000..6c15fcd3 --- /dev/null +++ b/docs/wired_full_reference.html @@ -0,0 +1,500 @@ + + + + + + Riferimento Completo Wired + + + + + + +
+
+
+
+
+ + Documentazione tecnica Wired +
+

+ Riferimento Completo Wired +

+

+ Questa pagina renderizza wired_full_reference.md in una vista HTML consultabile, + con struttura e interfaccia in italiano. Gli identificatori tecnici dei wired, delle classi e + delle chiavi restano invariati per mantenere la documentazione fedele al runtime. +

+
+
+
+
Sorgente
+
Markdown vivo
+
La pagina legge il file `.md` locale.
+
+
+
Lingua
+
Interfaccia italiana
+
Struttura e metadati localizzati.
+
+
+
Stile
+
Tailwind CDN
+
Layout leggibile, sticky nav e indice.
+
+
+
+
+ +
+ + +
+
+
+
+

Panoramica

+

+ Trovi all’inizio le regole del motore wired, poi il catalogo completo di trigger, effect, + selector, condition, extra e variabili. +

+
+
+ Engine + Tick + 154 wired catalogati + HTML da Markdown +
+
+
+ +
+
+ Caricamento della reference in corso... +
+ + +
+
+
+
+ + + + diff --git a/docs/wired_full_reference.md b/docs/wired_full_reference.md new file mode 100644 index 00000000..d27d60cf --- /dev/null +++ b/docs/wired_full_reference.md @@ -0,0 +1,1429 @@ +# Wired Full Reference + +## 1. Scope + +This document is a code-based reference for the current wired runtime in `Arcturus-Morningstar-Extended`. + +It covers: + +- general wired engine rules +- tick and delay rules +- protection and monitor rules +- custom variable rules +- every registered wired trigger, effect, selector, condition, extra, and variable definition + +Primary runtime sources used for this reference: + +- `Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java` + +This file is meant to describe the runtime behavior and configuration surface, not the Nitro UI layout in detail. For `:wired` monitor and inspection tooling, also see `Arcturus-Morningstar-Extended/docs/wired_tools_reference.md`. + +--- + +## 2. Wired Engine, Tick, and General Runtime Rules + +### 2.1 Main runtime architecture + +The modern wired runtime is centered around these components: + +- `WiredManager` + - initializes the wired runtime + - loads config + - owns the centralized engine and stack index + - exposes high-level trigger methods such as user click, walk on, say, signal, game events, and so on +- `WiredEngine` + - receives `WiredEvent` objects + - finds candidate stacks through the room stack index + - evaluates selectors, conditions, extras, and effects + - enforces abuse protection, delayed queue limits, recursion limits, and diagnostics +- `RoomWiredStackIndex` + - caches stack membership so the engine can quickly find candidate stacks for a given event type +- `WiredTickService` + - runs a single global tick loop + - keeps repeaters and other tickables synchronized across rooms +- `WiredHandler` + - legacy compatibility path that still exists in the codebase + - still useful to understand older stack execution logic and some compatibility behavior + +### 2.2 Current execution model + +At a high level, the engine processes a stack like this: + +1. receive an event +2. find candidate stacks for the event type +3. check whether the trigger matches +4. build a `WiredContext` +5. run selectors first, so the target set is available +6. apply selection-filter extras if present +7. evaluate conditions +8. apply stack-level gates such as execution limit +9. activate the trigger and extras +10. execute or schedule effects + +Important consequences of this model: + +- selectors run before conditions in the new engine +- conditions can inspect the selected targets or even the whole stack +- effects may run immediately or with delay +- extras can modify selection, condition evaluation, effect ordering, and effect subset choice + +### 2.3 Tick rules + +The centralized tick service is defined in `WiredTickService`. + +Current core rules: + +- default global tick interval: `50ms` +- hard allowed range: `10ms` to `500ms` +- repeaters and other tickables use a shared global tick counter +- tickables are registered per room +- when a room is unloaded, tickables should be unregistered + +This means: + +- repeaters are synchronized with each other +- two repeaters with the same timing do not drift independently per room +- unload/cleanup behavior matters for room-scoped temporary state + +### 2.4 Delay rules + +The classic wired delay value is stored in half-second steps. + +Runtime rule: + +- `effective delay in milliseconds = delay * 500` + +Examples: + +- delay `0` = immediate execution +- delay `1` = `500ms` +- delay `2` = `1000ms` + +This rule is used both by the legacy path and by the new engine. + +### 2.5 Stack ordering rules + +There are several separate notions of order: + +- **stack candidate order** + - candidate stacks are found through the index for the specific event type +- **item ordering inside one tile stack** + - `WiredExecutionOrderUtil` sorts by: + - `z` + - then `item id` +- **effect subset modifiers** + - `wf_xtra_random` can choose only part of the effects + - `wf_xtra_unseen` can rotate through effects without repeats +- **ordered execution** + - `wf_xtra_exec_in_order` is the explicit “run in stable stack order” modifier + +Practical takeaway: + +- if there is no order modifier, execution may depend on the collection/order produced by the runtime path +- if exact order matters, `wf_xtra_exec_in_order` is the intended box to use + +### 2.6 Selection rules + +Selectors build or refine the `WiredTargets` inside `WiredContext`. + +In practice: + +- target users and target furni are built before conditions are checked +- later effects consume those selected targets +- some selectors are pure “build a selection” tools +- some extras then trim, sort, or invert that selection + +### 2.7 Condition rules + +Conditions are evaluated after selectors. + +General behavior: + +- if there are no conditions, the stack can continue directly to effects +- if there are conditions, all configured condition logic must pass according to the current evaluation mode +- `wf_xtra_or_eval` changes how the condition results are aggregated + +The runtime supports both: + +- ordinary condition matching +- grouped OR semantics through condition operators and the OR-eval extra + +### 2.8 Protection rules + +The wired runtime has multiple safety layers: + +- maximum steps per stack +- recursion depth protection +- per-room event rate limiting +- room temporary wired ban after abuse +- delayed queue cap +- execution budget / usage cap per room window + +Main defaults from runtime/config: + +- `wired.engine.maxStepsPerStack = 100` +- `wired.abuse.max.recursion.depth = 10` +- `wired.abuse.max.events.per.window = 100` +- `wired.abuse.rate.limit.window.ms = 10000` +- `wired.abuse.ban.duration.ms = 600000` +- `wired.monitor.usage.window.ms = 1000` +- `wired.monitor.usage.limit = 1000` +- `wired.monitor.delayed.events.limit = 100` + +### 2.9 Monitor and diagnostics rules + +The new engine tracks room diagnostics through `WiredRoomDiagnostics`. + +This is where `:wired` monitor gets values such as: + +- usage in current window +- delayed events count +- average execution time +- peak execution time +- recursion state +- heavy room status +- overload windows + +Heavy/overload decisions are based on rolling windows, not on a single event. + +### 2.10 Legacy compatibility notes + +The project still contains `WiredHandler`. + +Important practical notes: + +- `WiredManager` is the intended modern entrypoint +- `wired.engine.enabled` and `wired.engine.exclusive` are treated as compatibility-only flags by `WiredManager` +- `WiredHandler` still exists and is useful for compatibility and for understanding legacy behavior + +So when documenting stacks, it is best to think in terms of: + +- modern runtime: `WiredManager` + `WiredEngine` +- legacy compatibility surface: `WiredHandler` + +### 2.11 Custom variable rules + +Custom wired variables are defined by: + +- `wf_var_user` +- `wf_var_furni` +- `wf_var_room` + +Shared rules: + +- variable names must be unique across the whole room, even across different variable types +- allowed name length: `1..40` +- allowed characters: letters, numbers, `_` + +Availability rules: + +- `wf_var_user` + - room-scoped while the user is in the room + - or permanent +- `wf_var_furni` + - room-active while the room is active/loaded + - or permanent +- `wf_var_room` + - room-active + - or permanent + +Timestamp rules: + +- user variables: creation/update are tied to the assignment on that user +- furni variables: creation/update are tied to the assignment on that furni +- room variables: practically meaningful timestamp is mainly the last update time + +Current context-status note: + +- `context` appears in several variable-related layouts +- it is still partial / placeholder in several runtime paths +- `user`, `furni`, and `room/global` are the truly active targets today + +### 2.12 Useful global config keys + +| Key | Meaning | +|---|---| +| `wired.engine.enabled` | Compatibility-only legacy flag | +| `wired.engine.exclusive` | Compatibility-only legacy flag | +| `wired.engine.maxStepsPerStack` | Loop/step protection limit | +| `wired.engine.debug` | Verbose engine logging | +| `wired.custom.enabled` | Legacy custom wired compatibility behavior | +| `hotel.wired.furni.selection.count` | Max furni selection size stored by wired boxes | +| `hotel.wired.max_delay` | Max accepted delay value | +| `hotel.wired.message.max_length` | Max wired/bot text size | +| `wired.effect.teleport.delay` | Teleport effect delay | +| `wired.tick.interval.ms` | Global tick loop interval | +| `wired.tick.debug` | Tick debug logging | +| `wired.tick.thread.priority` | Tick thread priority | +| `wired.abuse.max.recursion.depth` | Recursion protection | +| `wired.abuse.max.events.per.window` | Event spam protection | +| `wired.abuse.rate.limit.window.ms` | Abuse window size | +| `wired.abuse.ban.duration.ms` | Temporary room wired-ban duration | +| `wired.monitor.usage.window.ms` | Usage monitor window size | +| `wired.monitor.usage.limit` | Execution budget per window | +| `wired.monitor.delayed.events.limit` | Delayed queue ceiling | +| `wired.monitor.overload.average.ms` | Overload average threshold | +| `wired.monitor.overload.peak.ms` | Overload peak threshold | +| `wired.monitor.heavy.usage.percent` | Heavy-room usage threshold | +| `wired.highscores.displaycount` | Wired highscore rows shown to users | + +--- + +## 3. Triggers + +### `wf_trg_walks_on_furni` + +- **Class:** `WiredTriggerHabboWalkOnFurni` +- **Behavior:** fires when a user walks onto the selected furni/tile stack. +- **Main settings:** selected furni, standard trigger cooldown. +- **Notes:** commonly used as the first event in movement or pressure-style stacks. + +### `wf_trg_walks_off_furni` + +- **Class:** `WiredTriggerHabboWalkOffFurni` +- **Behavior:** fires when a user leaves the selected furni/tile stack. +- **Main settings:** selected furni, standard trigger cooldown. +- **Notes:** useful for exit logic, cleanup logic, and delayed “leave area” patterns. + +### `wf_trg_click_furni` + +- **Class:** `WiredTriggerHabboClicksFurni` +- **Behavior:** fires when a user clicks a furni. +- **Main settings:** selected furni. +- **Notes:** click-based stacks often combine this with selectors or trigger-user conditions. + +### `wf_trg_click_tile` + +- **Class:** `WiredTriggerHabboClicksTile` +- **Behavior:** fires when a user clicks a tile. +- **Main settings:** selected click-tile furni / trigger area depending on setup. +- **Notes:** often used for invisible tile-style interactions. + +### `wf_trg_click_user` + +- **Class:** `WiredTriggerHabboClicksUser` +- **Behavior:** fires when one avatar clicks another avatar. +- **Main settings:** runtime flags such as menu blocking and rotation behavior. +- **Notes:** the event carries both the clicking user and the clicked user. + +### `wf_trg_user_performs_action` + +- **Class:** `WiredTriggerHabboPerformsAction` +- **Behavior:** fires when a user performs a configured avatar action. +- **Main settings:** action id and action parameter. +- **Notes:** pairs naturally with the matching positive/negative action conditions. + +### `wf_trg_enter_room` + +- **Class:** `WiredTriggerHabboEntersRoom` +- **Behavior:** fires when a user enters the room. +- **Main settings:** none beyond default cooldown. +- **Notes:** common for welcome logic, spawn logic, variable assignment, and snapshot restore. + +### `wf_trg_leave_room` + +- **Class:** `WiredTriggerHabboLeavesRoom` +- **Behavior:** fires when a user leaves the room. +- **Main settings:** none beyond default cooldown. +- **Notes:** common for cleanup and last-known-state stacks. + +### `wf_trg_says_something` + +- **Class:** `WiredTriggerHabboSaysKeyword` +- **Behavior:** fires when a user says the configured text/keyword. +- **Main settings:** text/keyword, message hiding mode. +- **Notes:** can optionally suppress the visible chat output when configured to hide the message. + +### `wf_trg_clock_counter` + +- **Class:** `WiredTriggerClockCounter` +- **Behavior:** fires when a selected counter reaches its configured match point. +- **Main settings:** target counter(s), counter matching behavior. +- **Notes:** often combined with `wf_act_control_clock` and `wf_act_adjust_clock`. + +### `wf_trg_periodically` + +- **Class:** `WiredTriggerRepeater` +- **Behavior:** fires on a repeating interval. +- **Main settings:** repeat interval. +- **Notes:** synchronized through the global tick service. + +### `wf_trg_period_short` + +- **Class:** `WiredTriggerRepeaterShort` +- **Behavior:** faster repeating trigger with short cadence. +- **Main settings:** short repeater timing. +- **Notes:** aligned to the global `50ms` tick service. + +### `wf_trg_period_long` + +- **Class:** `WiredTriggerRepeaterLong` +- **Behavior:** repeating trigger with longer cadence. +- **Main settings:** long repeater timing. +- **Notes:** intended for lower-frequency repeating behavior. + +### `wf_trg_state_changed` + +- **Class:** `WiredTriggerFurniStateToggled` +- **Behavior:** fires when the state of the selected furni changes. +- **Main settings:** selected furni. +- **Notes:** runtime is shared with `wf_trg_stuff_state`. + +### `wf_trg_stuff_state` + +- **Class:** `WiredTriggerFurniStateToggled` +- **Behavior:** same runtime behavior as `wf_trg_state_changed`. +- **Main settings:** selected furni. +- **Notes:** kept as a second key/alias for compatibility/content mapping. + +### `wf_trg_at_given_time` + +- **Class:** `WiredTriggerAtSetTime` +- **Behavior:** fires once after the configured time target is reached. +- **Main settings:** time value. +- **Notes:** behaves like a one-shot timer rather than a repeater. + +### `wf_trg_at_time_long` + +- **Class:** `WiredTriggerAtTimeLong` +- **Behavior:** long-duration variant of the set-time trigger. +- **Main settings:** time value. +- **Notes:** used when the short version is not sufficient for the desired range. + +### `wf_trg_collision` + +- **Class:** `WiredTriggerCollision` +- **Behavior:** fires when the configured collision case is detected. +- **Main settings:** collision participants / collision mode. +- **Notes:** can easily produce loops when combined with chase/flee unless protections are configured. + +### `wf_trg_game_starts` + +- **Class:** `WiredTriggerGameStarts` +- **Behavior:** fires when the room game starts. +- **Main settings:** none beyond default cooldown. +- **Notes:** useful for score resets, timers, and spawn setup. + +### `wf_trg_game_ends` + +- **Class:** `WiredTriggerGameEnds` +- **Behavior:** fires when the room game ends. +- **Main settings:** none beyond default cooldown. +- **Notes:** useful for rewards, cleanup, and reset logic. + +### `wf_trg_bot_reached_stf` + +- **Class:** `WiredTriggerBotReachedFurni` +- **Behavior:** fires when a bot reaches the selected furni. +- **Main settings:** bot path target furni. +- **Notes:** typically paired with bot movement effects. + +### `wf_trg_bot_reached_avtr` + +- **Class:** `WiredTriggerBotReachedHabbo` +- **Behavior:** fires when a bot reaches an avatar. +- **Main settings:** target avatar/source mode. +- **Notes:** useful for escort, interaction, or story-style flows. + +### `wf_trg_score_achieved` + +- **Class:** `WiredTriggerScoreAchieved` +- **Behavior:** fires when the configured score threshold is reached. +- **Main settings:** score threshold. +- **Notes:** usually tied to game or team score flows. + +### `wf_trg_game_team_win` + +- **Class:** `WiredTriggerTeamWins` +- **Behavior:** fires when a team wins the current room game. +- **Main settings:** target team. +- **Notes:** can be used for reward or celebration logic. + +### `wf_trg_game_team_lose` + +- **Class:** `WiredTriggerTeamLoses` +- **Behavior:** fires when a team loses the current room game. +- **Main settings:** target team. +- **Notes:** often paired with reset or consolation logic. + +### `wf_trg_recv_signal` + +- **Class:** `WiredTriggerReceiveSignal` +- **Behavior:** fires when a matching signal is received from `wf_act_send_signal`. +- **Main settings:** selected antenna(s), signal/channel matching. +- **Notes:** can receive user/furni payload carried by the signal event. + +--- + +## 4. Effects + +### `wf_act_toggle_state` + +- **Class:** `WiredEffectToggleFurni` +- **Behavior:** toggles the state of the selected furni. +- **Main settings:** selected furni, effect delay. +- **Notes:** one of the most common state-manipulation effects. + +### `wf_act_reset_timers` + +- **Class:** `WiredEffectResetTimers` +- **Behavior:** resets compatible timer/repeater-style boxes. +- **Main settings:** selected timer/counter/repeater items. +- **Notes:** used to restart timing flows cleanly. + +### `wf_act_match_to_sshot` + +- **Class:** `WiredEffectMatchFurni` +- **Behavior:** restores furni to a saved snapshot of state/position/rotation settings. +- **Main settings:** selected furni, snapshot match mode/settings. +- **Notes:** usually paired with move/rotate or state-change effects. + +### `wf_act_move_rotate` + +- **Class:** `WiredEffectMoveRotateFurni` +- **Behavior:** moves and/or rotates furni according to the configured pattern. +- **Main settings:** selected furni, movement direction, rotation behavior, effect delay. +- **Notes:** obeys move physics extras when present. + +### `wf_act_give_score` + +- **Class:** `WiredEffectGiveScore` +- **Behavior:** gives score to the target user/player. +- **Main settings:** score amount. +- **Notes:** room/game scoring effect. + +### `wf_act_show_message` + +- **Class:** `WiredEffectWhisper` +- **Behavior:** sends the configured message text. +- **Main settings:** message text, effect delay. +- **Notes:** text length is limited by wired message config. + +### `wf_act_teleport_to` + +- **Class:** `WiredEffectTeleport` +- **Behavior:** teleports the target user to the configured destination. +- **Main settings:** target furni/tile, effect delay. +- **Notes:** also respects `wired.effect.teleport.delay`. + +### `wf_act_join_team` + +- **Class:** `WiredEffectJoinTeam` +- **Behavior:** moves the target user into the selected team. +- **Main settings:** team id/color. +- **Notes:** game-specific utility effect. + +### `wf_act_leave_team` + +- **Class:** `WiredEffectLeaveTeam` +- **Behavior:** removes the target user from their team. +- **Main settings:** effect delay. +- **Notes:** typically used in game cleanup. + +### `wf_act_chase` + +- **Class:** `WiredEffectMoveFurniTowards` +- **Behavior:** moves furni toward the configured target. +- **Main settings:** selected furni, target source, movement distance/direction rules. +- **Notes:** can interact strongly with collision and movement validation. + +### `wf_act_flee` + +- **Class:** `WiredEffectMoveFurniAway` +- **Behavior:** moves furni away from the configured target. +- **Main settings:** selected furni, target source, movement rules. +- **Notes:** often paired with collision or proximity triggers. + +### `wf_act_move_to_dir` + +- **Class:** `WiredEffectChangeFurniDirection` +- **Behavior:** changes furni direction/rotation. +- **Main settings:** selected furni, new direction or direction mode. +- **Notes:** pure direction-change effect without full movement pathing. + +### `wf_act_give_score_tm` + +- **Class:** `WiredEffectGiveScoreToTeam` +- **Behavior:** gives score directly to a team. +- **Main settings:** team id and score amount. +- **Notes:** separate from single-user score. + +### `wf_act_toggle_to_rnd` + +- **Class:** `WiredEffectToggleRandom` +- **Behavior:** toggles a random compatible furni among the selected set. +- **Main settings:** selected furni. +- **Notes:** randomness is per execution. + +### `wf_act_move_furni_to` + +- **Class:** `WiredEffectMoveFurniTo` +- **Behavior:** moves furni to a configured target position. +- **Main settings:** selected furni, destination tile/furni, effect delay. +- **Notes:** works with movement physics and animation extras. + +### `wf_act_give_reward` + +- **Class:** `WiredEffectGiveReward` +- **Behavior:** gives a configured reward. +- **Main settings:** reward type, reward content, amount, inventory/catalog parameters depending on reward mode. +- **Notes:** may generate inventory items, badges, or related reward outputs depending on configuration. + +### `wf_act_call_stacks` + +- **Class:** `WiredEffectTriggerStacks` +- **Behavior:** triggers other stacks indirectly. +- **Main settings:** selected furni/tile sources. +- **Notes:** recursion protection is important here. + +### `wf_act_kick_user` + +- **Class:** `WiredEffectKickHabbo` +- **Behavior:** kicks the target user from the room. +- **Main settings:** target source, effect delay. +- **Notes:** administrative/gameplay removal effect. + +### `wf_act_mute_triggerer` + +- **Class:** `WiredEffectMuteHabbo` +- **Behavior:** mutes the target user. +- **Main settings:** mute duration / target source depending on layout. +- **Notes:** often used in moderation or mini-game penalties. + +### `wf_act_bot_teleport` + +- **Class:** `WiredEffectBotTeleport` +- **Behavior:** teleports the selected bot. +- **Main settings:** bot source and destination. +- **Notes:** bot-only effect. + +### `wf_act_bot_move` + +- **Class:** `WiredEffectBotWalkToFurni` +- **Behavior:** makes a bot walk toward the selected furni. +- **Main settings:** bot source, target furni. +- **Notes:** commonly paired with bot reached triggers. + +### `wf_act_bot_talk` + +- **Class:** `WiredEffectBotTalk` +- **Behavior:** makes a bot say configured text. +- **Main settings:** bot source, message text. +- **Notes:** subject to wired/bot text size limits. + +### `wf_act_bot_give_handitem` + +- **Class:** `WiredEffectBotGiveHandItem` +- **Behavior:** gives a handitem to a bot. +- **Main settings:** bot source, handitem id. +- **Notes:** bot cosmetic / state effect. + +### `wf_act_bot_follow_avatar` + +- **Class:** `WiredEffectBotFollowHabbo` +- **Behavior:** makes a bot follow an avatar. +- **Main settings:** bot source, avatar source. +- **Notes:** useful for escort or scripted behaviors. + +### `wf_act_bot_clothes` + +- **Class:** `WiredEffectBotClothes` +- **Behavior:** changes a bot’s clothes/look. +- **Main settings:** bot source, look string. +- **Notes:** bot appearance effect. + +### `wf_act_bot_talk_to_avatar` + +- **Class:** `WiredEffectBotTalkToHabbo` +- **Behavior:** makes a bot talk toward an avatar/target. +- **Main settings:** bot source, avatar target, text. +- **Notes:** dialogue-oriented bot effect. + +### `wf_act_give_respect` + +- **Class:** `WiredEffectGiveRespect` +- **Behavior:** gives respect to the target user. +- **Main settings:** respect amount / target source. +- **Notes:** social reward effect. + +### `wf_act_alert` + +- **Class:** `WiredEffectAlert` +- **Behavior:** sends an alert window/message. +- **Main settings:** alert text. +- **Notes:** distinct from whisper-style chat output. + +### `wf_act_give_handitem` + +- **Class:** `WiredEffectGiveHandItem` +- **Behavior:** gives a handitem to the target user. +- **Main settings:** handitem id. +- **Notes:** user state/cosmetic effect. + +### `wf_act_give_effect` + +- **Class:** `WiredEffectGiveEffect` +- **Behavior:** gives an avatar effect to the target user. +- **Main settings:** effect id. +- **Notes:** visual avatar effect. + +### `wf_act_freeze` + +- **Class:** `WiredEffectFreeze` +- **Behavior:** freezes the selected user targets. +- **Main settings:** target source, effect delay. +- **Notes:** mainly game/control utility. + +### `wf_act_unfreeze` + +- **Class:** `WiredEffectUnfreeze` +- **Behavior:** unfreezes the selected user targets. +- **Main settings:** target source, effect delay. +- **Notes:** counterpart to `wf_act_freeze`. + +### `wf_act_furni_to_user` + +- **Class:** `WiredEffectFurniToUser` +- **Behavior:** moves furni toward/on a user target. +- **Main settings:** furni source, user source, effect delay. +- **Notes:** movement batching/physics extras may change the visible result. + +### `wf_act_user_to_furni` + +- **Class:** `WiredEffectUserToFurni` +- **Behavior:** moves a user toward a furni target. +- **Main settings:** user source, furni target. +- **Notes:** a user-targeted movement effect. + +### `wf_act_furni_to_furni` + +- **Class:** `WiredEffectFurniToFurni` +- **Behavior:** moves one furni set onto another furni set. +- **Main settings:** primary furni source, secondary furni source. +- **Notes:** supports double-selection source flow. + +### `wf_act_set_altitude` + +- **Class:** `WiredEffectSetAltitude` +- **Behavior:** sets furni altitude. +- **Main settings:** selected furni, altitude value or altitude mode. +- **Notes:** used in advanced movement / stacking setups. + +### `wf_act_rel_mov` + +- **Class:** `WiredEffectRelativeMove` +- **Behavior:** moves furni using relative X/Y offsets. +- **Main settings:** selected furni, X offset, Y offset. +- **Notes:** easier to reason about than absolute destination when building movement loops. + +### `wf_act_control_clock` + +- **Class:** `WiredEffectControlClock` +- **Behavior:** controls counter boxes. +- **Main settings:** selected counter(s), action mode such as start/stop/reset/pause/resume. +- **Notes:** works directly with counter-based trigger/condition flows. + +### `wf_act_adjust_clock` + +- **Class:** `WiredEffectAdjustClock` +- **Behavior:** adjusts a counter’s current value. +- **Main settings:** selected counter(s), operation mode, amount. +- **Notes:** intended for dynamic counter manipulation. + +### `wf_act_move_rotate_user` + +- **Class:** `WiredEffectMoveRotateUser` +- **Behavior:** moves and/or rotates user targets. +- **Main settings:** user source, movement mode, direction/rotation settings. +- **Notes:** user-side analogue of furni move/rotate logic. + +### `wf_act_send_signal` + +- **Class:** `WiredEffectSendSignal` +- **Behavior:** sends a signal through antenna-based wiring. +- **Main settings:** selected antenna furni, signal payload/source options. +- **Notes:** can carry user/furni payload to `wf_trg_recv_signal`. + +### `wf_act_give_var` + +- **Class:** `WiredEffectGiveVariable` +- **Behavior:** assigns a custom variable to a compatible target. +- **Main settings:** variable definition, target type/source, overwrite flag, initial value if the variable has value. +- **Notes:** works with `wf_var_user` and `wf_var_furni`; room/global variables are definition-driven and do not need this assigner. + +### `wf_act_remove_var` + +- **Class:** `WiredEffectRemoveVariable` +- **Behavior:** removes a custom variable assignment from the selected target. +- **Main settings:** variable definition, target type/source. +- **Notes:** counterpart to `wf_act_give_var`. + +### `wf_act_change_var_val` + +- **Class:** `WiredEffectChangeVariableValue` +- **Behavior:** changes the value of a variable by applying an operation. +- **Main settings:** variable selection, operation, reference mode, constant or reference variable, reference source, target source. +- **Supported operations:** assign, add, subtract, multiply, divide, power, modulo, min, max, random, absolute, bitwise AND/OR/XOR/NOT, left shift, right shift. +- **Notes:** one of the most flexible variable effects; textual rendering is separate and handled by extras. + +--- + +## 5. Selectors + +### General selector notes + +Selectors typically do one or both of these: + +- build a new target set +- filter/transform an existing target set + +When the UI exposes classic selector options, those usually include: + +- filter the existing selection +- invert the result + +### `wf_slc_furni_area` + +- **Class:** `WiredEffectFurniArea` +- **Behavior:** selects furni in a configured area. +- **Main settings:** area size/position. +- **Notes:** foundational room-space selector. + +### `wf_slc_furni_neighborhood` + +- **Class:** `WiredEffectFurniNeighborhood` +- **Behavior:** selects furni in a local neighborhood around the source point. +- **Main settings:** neighborhood/radius. +- **Notes:** useful for adjacency-based logic. + +### `wf_slc_furni_bytype` + +- **Class:** `WiredEffectFurniByType` +- **Behavior:** selects furni by base furni type. +- **Main settings:** furni type. +- **Notes:** good for “all chairs”, “all switches”, and similar patterns. + +### `wf_slc_furni_altitude` + +- **Class:** `WiredEffectFurniAltitude` +- **Behavior:** selects furni by altitude relation/value. +- **Main settings:** compare mode and altitude target. +- **Notes:** useful in stacked build logic. + +### `wf_slc_furni_onfurni` + +- **Class:** `WiredEffectFurniOnFurni` +- **Behavior:** selects furni that are on top of other furni. +- **Main settings:** base furni selection. +- **Notes:** stack-inspection selector. + +### `wf_slc_furni_picks` + +- **Class:** `WiredEffectFurniPicks` +- **Behavior:** selects a hand-picked list of furni. +- **Main settings:** selected furni list. +- **Notes:** capped by `hotel.wired.furni.selection.count`. + +### `wf_slc_furni_signal` + +- **Class:** `WiredEffectFurniSignal` +- **Behavior:** selects furni carried by a signal event. +- **Main settings:** signal source mode. +- **Notes:** meaningful only in signal-driven stacks. + +### `wf_slc_users_area` + +- **Class:** `WiredEffectUsersArea` +- **Behavior:** selects users in a configured area. +- **Main settings:** area size/position. +- **Notes:** area equivalent of the furni selector. + +### `wf_slc_users_neighborhood` + +- **Class:** `WiredEffectUsersNeighborhood` +- **Behavior:** selects users in a nearby neighborhood. +- **Main settings:** neighborhood/radius. +- **Notes:** good for local interaction logic. + +### `wf_slc_users_signal` + +- **Class:** `WiredEffectUsersSignal` +- **Behavior:** selects users carried by a signal event. +- **Main settings:** signal source mode. +- **Notes:** signal-only context. + +### `wf_slc_users_bytype` + +- **Class:** `WiredEffectUsersByType` +- **Behavior:** selects users by runtime category. +- **Main settings:** user type such as habbo, bot, pet. +- **Notes:** useful for mixed rooms with bots and pets. + +### `wf_slc_users_team` + +- **Class:** `WiredEffectUsersTeam` +- **Behavior:** selects users by team membership. +- **Main settings:** team id/color. +- **Notes:** game-centric selector. + +### `wf_slc_users_byaction` + +- **Class:** `WiredEffectUsersByAction` +- **Behavior:** selects users by current action/state. +- **Main settings:** action type / action parameter. +- **Notes:** complements the action trigger/conditions. + +### `wf_slc_users_byname` + +- **Class:** `WiredEffectUsersByName` +- **Behavior:** selects users whose names are listed in the text area. +- **Main settings:** multiline list of usernames. +- **Notes:** direct name-driven selector. + +### `wf_slc_users_handitem` + +- **Class:** `WiredEffectUsersHandItem` +- **Behavior:** selects users holding a specific handitem. +- **Main settings:** handitem id. +- **Notes:** useful for role/item possession flows. + +### `wf_slc_users_onfurni` + +- **Class:** `WiredEffectUsersOnFurni` +- **Behavior:** selects users standing on selected furni. +- **Main settings:** base furni selection. +- **Notes:** common in pressure/tile gameplay. + +### `wf_slc_users_group` + +- **Class:** `WiredEffectUsersGroup` +- **Behavior:** selects users by group relationship in the room. +- **Main settings:** group relation/mode. +- **Notes:** useful for rights/group-room logic. + +### `wf_slc_furni_with_var` + +- **Class:** `WiredEffectFurniWithVariable` +- **Behavior:** selects furni that hold a chosen custom variable. +- **Main settings:** variable selection, optional value filter, comparison operator, constant or variable reference, reference source, selector options. +- **Notes:** if value filtering is disabled, it behaves as a presence-only selector. + +### `wf_slc_users_with_var` + +- **Class:** `WiredEffectUsersWithVariable` +- **Behavior:** selects users that hold a chosen custom variable. +- **Main settings:** variable selection, optional value filter, comparison operator, constant or variable reference, reference source, selector options. +- **Notes:** user-side analogue of the furni variable selector. + +--- + +## 6. Conditions + +### General condition notes + +Conditions can be thought of as gates for the stack. + +Common patterns: + +- positive/negative counterpart pairs +- threshold checks +- “match the current selection” +- variable-based checks +- time/date checks + +### `wf_cnd_has_furni_on` + +- **Class:** `WiredConditionFurniHaveFurni` +- **Behavior:** true if the configured furni have other furni on top. +- **Main settings:** target furni selection. + +### `wf_cnd_furnis_hv_avtrs` + +- **Class:** `WiredConditionFurniHaveHabbo` +- **Behavior:** true if the configured furni currently have avatars on top. +- **Main settings:** target furni selection. + +### `wf_cnd_stuff_is` + +- **Class:** `WiredConditionFurniTypeMatch` +- **Behavior:** true if the furni match the configured type. +- **Main settings:** furni type. + +### `wf_cnd_actor_in_group` + +- **Class:** `WiredConditionGroupMember` +- **Behavior:** true if the acting user is in the required group relation. +- **Main settings:** group relation. + +### `wf_cnd_user_count_in` + +- **Class:** `WiredConditionHabboCount` +- **Behavior:** true if room user count satisfies the configured threshold. +- **Main settings:** comparison and count value. + +### `wf_cnd_wearing_effect` + +- **Class:** `WiredConditionHabboHasEffect` +- **Behavior:** true if the target user is wearing the configured effect. +- **Main settings:** effect id. + +### `wf_cnd_wearing_badge` + +- **Class:** `WiredConditionHabboWearsBadge` +- **Behavior:** true if the target user wears the configured badge. +- **Main settings:** badge code. + +### `wf_cnd_time_less_than` + +- **Class:** `WiredConditionLessTimeElapsed` +- **Behavior:** true if less than the configured time has elapsed. +- **Main settings:** duration. + +### `wf_cnd_match_snapshot` + +- **Class:** `WiredConditionMatchStatePosition` +- **Behavior:** true if the current furni state/position matches the stored snapshot. +- **Main settings:** selected furni, snapshot fields to compare. + +### `wf_cnd_time_more_than` + +- **Class:** `WiredConditionMoreTimeElapsed` +- **Behavior:** true if more than the configured time has elapsed. +- **Main settings:** duration. + +### `wf_cnd_not_furni_on` + +- **Class:** `WiredConditionNotFurniHaveFurni` +- **Behavior:** logical negation of `wf_cnd_has_furni_on`. +- **Main settings:** target furni selection. + +### `wf_cnd_not_hv_avtrs` + +- **Class:** `WiredConditionNotFurniHaveHabbo` +- **Behavior:** logical negation of `wf_cnd_furnis_hv_avtrs`. +- **Main settings:** target furni selection. + +### `wf_cnd_not_stuff_is` + +- **Class:** `WiredConditionNotFurniTypeMatch` +- **Behavior:** logical negation of `wf_cnd_stuff_is`. +- **Main settings:** furni type. + +### `wf_cnd_not_user_count` + +- **Class:** `WiredConditionNotHabboCount` +- **Behavior:** logical negation of the user-count match. +- **Main settings:** comparison and count value. + +### `wf_cnd_not_wearing_fx` + +- **Class:** `WiredConditionNotHabboHasEffect` +- **Behavior:** true if the user is not wearing the configured effect. +- **Main settings:** effect id. + +### `wf_cnd_not_wearing_b` + +- **Class:** `WiredConditionNotHabboWearsBadge` +- **Behavior:** true if the user is not wearing the configured badge. +- **Main settings:** badge code. + +### `wf_cnd_not_in_group` + +- **Class:** `WiredConditionNotInGroup` +- **Behavior:** true if the user is not in the configured group relation. +- **Main settings:** group relation. + +### `wf_cnd_not_in_team` + +- **Class:** `WiredConditionNotInTeam` +- **Behavior:** true if the user is not in the configured team. +- **Main settings:** team id/color. + +### `wf_cnd_not_match_snap` + +- **Class:** `WiredConditionNotMatchStatePosition` +- **Behavior:** logical negation of snapshot match. +- **Main settings:** selected furni, snapshot fields to compare. + +### `wf_cnd_not_trggrer_on` + +- **Class:** `WiredConditionNotTriggerOnFurni` +- **Behavior:** true if the triggerer is not on the selected furni. +- **Main settings:** selected furni. + +### `wf_cnd_actor_in_team` + +- **Class:** `WiredConditionTeamMember` +- **Behavior:** true if the actor belongs to the required team. +- **Main settings:** team id/color. + +### `wf_cnd_trggrer_on_frn` + +- **Class:** `WiredConditionTriggerOnFurni` +- **Behavior:** true if the triggerer is on the selected furni. +- **Main settings:** selected furni. + +### `wf_cnd_has_handitem` + +- **Class:** `WiredConditionHabboHasHandItem` +- **Behavior:** true if the user currently holds the configured handitem. +- **Main settings:** handitem id. + +### `wf_cnd_not_has_handitem` + +- **Class:** `WiredConditionNotHabboHasHandItem` +- **Behavior:** logical negation of the handitem condition. +- **Main settings:** handitem id. + +### `wf_cnd_date_rng_active` + +- **Class:** `WiredConditionDateRangeActive` +- **Behavior:** true if current server time is between the configured absolute date/time bounds. +- **Main settings:** start timestamp, end timestamp. + +### `wf_cnd_valid_moves` + +- **Class:** `WiredConditionMovementValidation` +- **Behavior:** simulates movement-related effects in the current stack and fails if a movement would be invalid. +- **Main settings:** no major user-facing setting besides stack composition. +- **Notes:** especially useful before move/rotate stacks. + +### `wf_cnd_counter_time_matches` + +- **Class:** `WiredConditionCounterTimeMatches` +- **Behavior:** true if the selected counter(s) match the configured time value. +- **Main settings:** counter selection, compare mode, target value, quantifier. + +### `wf_cnd_match_time` + +- **Class:** `WiredConditionMatchTime` +- **Behavior:** true if server local time matches the configured clock rule. +- **Main settings:** hour/minute/second or related time fields. + +### `wf_cnd_match_date` + +- **Class:** `WiredConditionMatchDate` +- **Behavior:** true if server local date matches the configured date rule. +- **Main settings:** weekday/day/month/year. + +### `wf_cnd_actor_dir` + +- **Class:** `WiredConditionActorDir` +- **Behavior:** true if the actor faces the configured direction. +- **Main settings:** direction. + +### `wf_cnd_slc_quantity` + +- **Class:** `WiredConditionSelectionQuantity` +- **Behavior:** true if the current selection size matches the configured threshold. +- **Main settings:** compare mode and amount. + +### `wf_cnd_user_performs_action` + +- **Class:** `WiredConditionUserPerformsAction` +- **Behavior:** true if the tracked user action matches. +- **Main settings:** action id / action parameter. + +### `wf_cnd_not_user_performs_action` + +- **Class:** `WiredConditionNotUserPerformsAction` +- **Behavior:** logical negation of the user action condition. +- **Main settings:** action id / action parameter. + +### `wf_cnd_has_altitude` + +- **Class:** `WiredConditionHasAltitude` +- **Behavior:** true if the selected furni satisfy the altitude comparison. +- **Main settings:** compare mode and altitude value. + +### `wf_cnd_triggerer_match` + +- **Class:** `WiredConditionTriggererMatch` +- **Behavior:** true if the triggerer matches the required target/source rule. +- **Main settings:** target source/match mode. + +### `wf_cnd_not_triggerer_match` + +- **Class:** `WiredConditionNotTriggererMatch` +- **Behavior:** logical negation of triggerer match. +- **Main settings:** target source/match mode. + +### `wf_cnd_team_has_score` + +- **Class:** `WiredConditionTeamHasScore` +- **Behavior:** true if the selected team score satisfies the configured comparison. +- **Main settings:** team id, comparison mode, score threshold. + +### `wf_cnd_team_has_rank` + +- **Class:** `WiredConditionTeamHasRank` +- **Behavior:** true if the selected team currently has the configured rank/placement. +- **Main settings:** team id, rank target. + +### `wf_cnd_has_var` + +- **Class:** `WiredConditionHasVariable` +- **Behavior:** true if the target entity holds the chosen variable. +- **Main settings:** variable selection, quantifier (`all` / `any`), variable source target. +- **Notes:** current layout/runtime is centered on user and furni variables; context exists as future placeholder. + +### `wf_cnd_neg_has_var` + +- **Class:** `WiredConditionNotHasVariable` +- **Behavior:** logical negation of `wf_cnd_has_var`. +- **Main settings:** variable selection, quantifier, source target. + +### `wf_cnd_var_val_match` + +- **Class:** `WiredConditionVariableValueMatch` +- **Behavior:** compares a variable value against a constant or another variable. +- **Main settings:** variable selection, compare type (`>`, `≥`, `=`, `≤`, `<`, `≠`), reference mode, reference variable/source, quantifier. +- **Notes:** room/global variables are supported here; context remains partial. + +### `wf_cnd_var_age_match` + +- **Class:** `WiredConditionVariableAgeMatch` +- **Behavior:** compares variable age against a duration. +- **Main settings:** variable selection, compare field (`creation` or `update` time), compare type (`lower than` / `higher than`), duration value + unit, quantifier, source. +- **Notes:** room/global variables are mostly meaningful for update time. + +--- + +## 7. Extras + +### `wf_xtra_random` + +- **Class:** `WiredExtraRandom` +- **Behavior:** executes only a random subset of effects instead of all effects. +- **Main settings:** number of effects to choose, optional recent-history protection. +- **Notes:** effect subset changes at each execution. + +### `wf_xtra_unseen` + +- **Class:** `WiredExtraUnseen` +- **Behavior:** rotates through effects without repeating one until the full cycle is exhausted. +- **Main settings:** hidden runtime state / no-repeat cycle. +- **Notes:** useful when true round-robin behavior is preferred over randomness. + +### `wf_blob` + +- **Class:** `WiredBlob` +- **Behavior:** special wired/game helper item. +- **Main settings:** blob-specific gameplay/runtime state. +- **Notes:** not a normal logic extra in the same sense as the others, but it is registered in the wired extra family. + +### `wf_xtra_or_eval` + +- **Class:** `WiredExtraOrEval` +- **Behavior:** changes how condition results are aggregated. +- **Main settings:** evaluation mode and compare value. +- **Notes:** lets stacks use modes beyond plain “all conditions must pass”. + +### `wf_xtra_filter_furni` + +- **Class:** `WiredExtraFilterFurni` +- **Behavior:** trims the current furni selection to a limited quantity. +- **Main settings:** quantity. +- **Notes:** selection-filter extra, not a normal selector. + +### `wf_xtra_filter_user` + +- **Class:** `WiredExtraFilterUser` +- **Behavior:** trims the current user selection to a limited quantity. +- **Main settings:** quantity. +- **Notes:** same runtime family as `wf_xtra_filter_users`. + +### `wf_xtra_filter_users` + +- **Class:** `WiredExtraFilterUser` +- **Behavior:** same runtime behavior as `wf_xtra_filter_user`. +- **Main settings:** quantity. +- **Notes:** alias key kept for content compatibility. + +### `wf_xtra_filter_furni_by_var` + +- **Class:** `WiredExtraFilterFurniByVariable` +- **Behavior:** sorts furni by variable metric and keeps only the top N. +- **Main settings:** variable selection, sort mode, quantity mode, constant quantity or variable reference, reference source. +- **Supported sort modes:** highest value, lowest value, oldest creation, latest creation, oldest update, latest update. + +### `wf_xtra_filter_users_by_var` + +- **Class:** `WiredExtraFilterUsersByVariable` +- **Behavior:** sorts users by variable metric and keeps only the top N. +- **Main settings:** variable selection, sort mode, quantity mode, constant quantity or variable reference, reference source. +- **Supported sort modes:** highest value, lowest value, oldest creation, latest creation, oldest update, latest update. + +### `wf_xtra_mov_carry_users` + +- **Class:** `WiredExtraMoveCarryUsers` +- **Behavior:** carries users together with moved furni. +- **Main settings:** carry mode. +- **Notes:** affects how movement results are applied when furni move. + +### `wf_xtra_mov_no_animation` + +- **Class:** `WiredExtraMoveNoAnimation` +- **Behavior:** suppresses movement animation. +- **Main settings:** none besides presence in stack. +- **Notes:** intended for instant or hidden movement behavior. + +### `wf_xtra_anim_time` + +- **Class:** `WiredExtraAnimationTime` +- **Behavior:** overrides movement animation time. +- **Main settings:** animation duration. +- **Notes:** influences visual pacing, not core selection logic. + +### `wf_xtra_mov_physics` + +- **Class:** `WiredExtraMovePhysics` +- **Behavior:** changes the physics rules applied during movement. +- **Main settings:** physics flags such as collision/pass-through/stack behavior depending on layout. +- **Notes:** important for advanced furni movement setups. + +### `wf_xtra_exec_in_order` + +- **Class:** `WiredExtraExecuteInOrder` +- **Behavior:** forces ordered effect execution. +- **Main settings:** none besides presence in stack. +- **Notes:** the explicit “do not rely on arbitrary order” extra. + +### `wf_xtra_execution_limit` + +- **Class:** `WiredExtraExecutionLimit` +- **Behavior:** allows the stack to execute only a configured number of times per window. +- **Main settings:** max executions, time window. +- **Notes:** stack-level throttle. + +### `wf_xtra_text_output_username` + +- **Class:** `WiredExtraTextOutputUsername` +- **Behavior:** exposes one or more usernames as a text placeholder for other wired text. +- **Main settings:** placeholder name, placeholder type (single/multiple), delimiter, user source. +- **Notes:** works like a text injector for later wired text output. + +### `wf_xtra_text_output_furni_name` + +- **Class:** `WiredExtraTextOutputFurniName` +- **Behavior:** exposes furni names as a text placeholder. +- **Main settings:** placeholder name, placeholder type (single/multiple), delimiter, furni source. +- **Notes:** furni-name counterpart to username output. + +### `wf_xtra_text_output_variable` + +- **Class:** `WiredExtraTextOutputVariable` +- **Behavior:** exposes a variable value as a text placeholder. +- **Main settings:** placeholder name, variable selection, display type (`numeric` / `textual`), placeholder type (`single` / `multiple`), delimiter, dynamic variable source. +- **Notes:** textual display works only when the selected variable is connected through `wf_xtra_var_text_connector`. + +### `wf_xtra_var_text_connector` + +- **Class:** `WiredExtraVariableTextConnector` +- **Behavior:** maps numeric values to text labels for a variable. +- **Main settings:** text area mapping in the form `0=text`, `1=text`, and so on. +- **Notes:** must live in the same stack context as the corresponding `wf_var_*` definition to be meaningful. + +--- + +## 8. Variable Definitions + +### `wf_var_user` + +- **Class:** `WiredExtraUserVariable` +- **Behavior:** defines a custom variable that can be assigned to users. +- **Main settings:** variable name, `has value` flag, availability (`while user is in room` / `permanent`). +- **Notes:** assignment is done through `wf_act_give_var`; timestamps belong to the assignment on the individual user. + +### `wf_var_furni` + +- **Class:** `WiredExtraFurniVariable` +- **Behavior:** defines a custom variable that can be assigned to furni. +- **Main settings:** variable name, `has value` flag, availability (`while room is active` / `permanent`). +- **Notes:** non-permanent assignments are cleaned when the room is unloaded/unregistered from room tickables. + +### `wf_var_room` + +- **Class:** `WiredExtraRoomVariable` +- **Behavior:** defines a room/global variable. +- **Main settings:** variable name, availability (`while room is active` / `permanent`). +- **Notes:** always has a value; there is no separate “has value” checkbox for room variables. + +--- + +## 9. Special Wired Items + +These are part of the wired ecosystem, even if they are not regular trigger/effect/selector/condition/extra boxes. + +### `wf_highscore` + +- **Class:** `InteractionWiredHighscore` +- **Behavior:** wired highscore furniture that stores and displays ranked score data. +- **Main settings:** score type, clear/reset policy, display behavior depending on furniture configuration. +- **Notes:** governed also by `wired.highscores.displaycount`. + +--- + +## 10. Practical Design Notes + +### 10.1 If exact order matters + +Use: + +- `wf_xtra_exec_in_order` + +Do not rely on “it seems to run in that order” when the stack becomes more complex. + +### 10.2 If the stack performs movement + +Prefer to think about: + +- movement validation +- movement physics extras +- carry-users extra +- animation/no-animation extras +- snapshot restore effects + +Movement stacks are where most subtle runtime interactions appear. + +### 10.3 If the stack uses variables + +Remember: + +- variable name must be room-unique +- target type matters +- room/global variables are definition-driven +- textual rendering requires the text connector +- `context` is not yet fully implemented everywhere + +### 10.4 If the stack uses repeaters/timers + +Remember: + +- repeaters are synchronized on the global tick loop +- delay units are half-seconds +- counters, repeaters, and timer-style triggers often need explicit reset/control logic + +### 10.5 If the stack is heavy + +Check: + +- selection size +- number of delayed effects +- recursion or self-trigger chains +- random/unseen subsets +- execution limits +- room diagnostics in `:wired` + +--- + +## 11. Quick Alias / Shared Runtime Notes + +- `wf_trg_state_changed` and `wf_trg_stuff_state` share the same runtime. +- `wf_xtra_filter_user` and `wf_xtra_filter_users` share the same runtime. +- Several positive/negative conditions are simple logical counterparts. +- `wf_act_give_var`, `wf_act_remove_var`, `wf_act_change_var_val`, variable selectors, and variable conditions all operate on top of the same custom variable system defined by `wf_var_*`. diff --git a/docs/wired_tools_implementation_summary.md b/docs/wired_tools_implementation_summary.md new file mode 100644 index 00000000..7dbc5c28 --- /dev/null +++ b/docs/wired_tools_implementation_summary.md @@ -0,0 +1,283 @@ +# Wired Creator Tools Implementation Summary + +## 1. Purpose + +This document summarizes the `:wired` work completed in this development cycle. + +It is intended as a project-facing summary of: + +- what was added +- where it lives +- what is already working +- what is still intentionally left as future work + +--- + +## 2. Main goals completed + +The current `:wired` implementation now provides: + +1. a dedicated Nitro UI window +2. monitor and inspection tooling +3. room/user/furni/global variable views +4. inline editing for selected values +5. live wired diagnostics from the server +6. error/warning history with details +7. server-side diagnostics configuration through DB settings + +--- + +## 3. Nitro-V3 work + +Main file: + +- `Nitro-V3/src/components/wired-tools/WiredCreatorToolsView.tsx` + +### 3.1 UI window + +The `:wired` tool now has these main tabs: + +- `Monitor` +- `Variables` +- `Inspection` +- `Chests` +- `Settings` + +Current active work is mainly in: + +- `Monitor` +- `Inspection` + +`Chests` and `Settings` are currently placeholder/future-facing areas. + +### 3.2 Inspection + +Implemented: + +- element type switcher (`furni`, `user`, `global`) +- preview area +- variable table +- `Keep selected` +- inline editing + +#### Furni + +Added support for: + +- detailed furni variables +- live preview +- wall/floor-specific handling +- teleport metadata +- inline edits for state/position/rotation/altitude/wall offset + +#### User + +Added support for: + +- user/bot/pet identity +- rights / owner / group admin flags +- mute / trading / frozen flags +- team / sign / dance / idle / hand item / effect display +- room entry method and teleport entry id +- inline edits for position and direction + +#### Global + +Added support for: + +- room counts +- wired timer +- team scores and sizes +- room/group ids +- server/client timezone +- current server time breakdown + +### 3.3 Monitor + +Implemented: + +- live stats table +- log summary list +- full log history +- info/documentation popup +- error information popup + +The auxiliary monitor windows now use proper Nitro card windows, so they are: + +- draggable +- resizable +- closed through the normal Nitro close button + +--- + +## 4. Nitro_Render_V3 work + +Renderer-side work was focused on making Nitro receive enough metadata for the new UI. + +Main areas: + +- new wired monitor packet parsing +- room/session metadata extensions +- furni metadata extensions +- user metadata extensions + +### 4.1 Monitor data + +The renderer now parses and exposes: + +- usage budget values +- delayed queue values +- execution timing values +- heavy/overload thresholds +- current logs +- history rows + +### 4.2 Room metadata + +The renderer/session flow was extended to expose values used by Nitro: + +- room furni limit +- room group id +- hotel timezone / hotel time snapshot + +### 4.3 Furni metadata + +The furni info path now exposes values used by the inspector, including: + +- dimensions +- `items_base`-driven flags such as sit/lay/stand/stack +- teleport target metadata + +### 4.4 User metadata + +The user/unit data path now exposes values used by the inspector, including: + +- room entry method +- room entry teleport id +- identity data for user/bot/pet + +--- + +## 5. Emulator work + +Main areas: + +- wired diagnostics engine +- monitor request/response packet +- room/user/furni metadata support +- configuration migration to `wired_emulator_settings` + +### 5.1 Wired diagnostics + +Added server-side room diagnostics with: + +- usage budget tracking +- delayed event queue tracking +- average/peak execution timing +- overload detection +- heavy-room detection +- recursion protection logging +- killed-room protection logging + +### 5.2 Diagnostics logs + +Logs now carry: + +- type +- severity +- count +- reason +- source label +- source id +- history entries with occurrence timestamps + +### 5.3 Trigger/runtime fixes + +Important behaviour fixes added during this work: + +- empty repeater stacks no longer count as executable work +- monitor usage is consumed later in the execution path, closer to real execution +- timer/repeater behaviour is less noisy in diagnostics + +### 5.4 Monitor packet + +A dedicated request/response path was added so Nitro can poll live room diagnostics. + +### 5.5 Configuration migration + +All wired config is being moved out of `emulator_settings` and into: + +- `wired_emulator_settings` + +This now includes both: + +- existing wired runtime settings +- the new `:wired` monitor threshold settings + +Migration file: + +- `Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql` + +--- + +## 6. What the monitor currently measures + +The monitor currently measures: + +- execution budget consumed in the current server window +- delayed events currently pending +- average execution time inside the current window +- peak execution time inside the current window +- recursion depth +- remaining killed-room cooldown +- room heavy state +- room furni counts +- renderer custom variable counts on room items + +--- + +## 7. What is configurable now + +Current DB-configurable areas include: + +- engine enable/debug/exclusive/max-steps +- custom wired compatibility mode +- furni selection limit +- max delay / max text length +- teleport delay +- tick interval/debug/priority +- abuse protection thresholds +- monitor usage/delayed/heavy/overload thresholds + +All of these are documented in: + +- `docs/wired_tools_reference.md` + +--- + +## 8. Known limitations / future work + +Current known limitations: + +- `Permanent furni vars` uses a fixed UI denominator (`60`) +- `@wired_timer` is still client-side time since room entry +- `Chests` and `Settings` are not fully implemented yet +- legacy wired configuration keys are still present for database compatibility, but runtime execution now goes only through the new engine + +Good future tasks: + +- make `Permanent furni vars` fully server-driven +- add export/copy actions for monitor history +- add more detailed filtering/search in history +- document chest/settings once implemented +- optionally remove the compatibility keys entirely once old database defaults are no longer needed + +--- + +## 9. Recommended rollout order + +1. run the wired settings migration SQL +2. restart the emulator +3. refresh renderer/client +4. verify monitor values in a real room +5. tune `wired.monitor.*` thresholds using the new DB table diff --git a/docs/wired_tools_reference.md b/docs/wired_tools_reference.md new file mode 100644 index 00000000..d5dbda23 --- /dev/null +++ b/docs/wired_tools_reference.md @@ -0,0 +1,554 @@ +# Wired Creator Tools (`:wired`) Reference + +## 1. Scope + +This document describes the current `:wired` tooling that was added across: + +- `Arcturus-Morningstar-Extended` (server-side data, diagnostics, config) +- `Nitro_Render_V3` (packet parsing and room/session metadata) +- `Nitro-V3` (UI, monitor, inspection, previews, inline editing) + +It focuses on: + +- the `Monitor` tab +- the `Inspection` tab (`furni`, `user`, `global`) +- the `wired_emulator_settings` database table +- the formulas and thresholds behind the monitor statistics + +--- + +## 2. High-level architecture + +### 2.1 Data flow + +`Emulator` -> `Nitro_Render_V3` -> `Nitro-V3` + +- The emulator computes room diagnostics and exposes extra room, furni, user, and monitor metadata. +- The renderer parses those packets and stores the values in room/session data objects. +- Nitro reads those values and renders them in the `:wired` UI. + +### 2.2 Main files + +- Emulator diagnostics: + - `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java` + - `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java` + - `Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java` +- Renderer parsing: + - `Nitro_Render_V3/packages/communication/src/messages/parser/roomevents/WiredMonitorDataParser.ts` +- Nitro UI: + - `Nitro-V3/src/components/wired-tools/WiredCreatorToolsView.tsx` + +--- + +## 3. Database configuration + +## 3.1 New table + +Wired configuration is now separated from `emulator_settings` into: + +```sql +wired_emulator_settings ( + key, + value, + comment +) +``` + +Migration file: + +- `Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql` + +The migration: + +1. creates `wired_emulator_settings` +2. imports existing wired values from `emulator_settings` +3. inserts defaults for new monitor/diagnostic keys when missing +4. removes migrated wired keys from `emulator_settings` + +### 3.2 Compatibility behaviour + +The emulator still has a **fallback read path**: + +- first it reads wired keys from `wired_emulator_settings` +- if a wired key is still missing there, it can still read it from the old `emulator_settings` + +This allows safe rollout during migration. + +## 3.3 Wired configuration keys + +| Key | Default | Purpose | +|---|---:|---| +| `wired.engine.enabled` | `1` | Compatibility flag kept for old configs. Wired now runs through the new engine only. | +| `wired.engine.exclusive` | `1` | Compatibility flag kept for old configs. Wired now runs through the new engine only. | +| `wired.engine.maxStepsPerStack` | `100` | Maximum internal processing steps allowed for one stack execution. | +| `wired.engine.debug` | `0` | Enables verbose wired engine logging. | +| `wired.custom.enabled` | `0` | Enables legacy custom wired compatibility logic. | +| `hotel.wired.furni.selection.count` | `5` | Maximum furni count selectable/storable by wired boxes. | +| `hotel.wired.max_delay` | `20` | Maximum accepted wired delay value for delayed effects. | +| `hotel.wired.message.max_length` | `100` | Maximum length of wired/bot text fields. | +| `wired.effect.teleport.delay` | `500` | Delay in milliseconds used by wired teleports. | +| `wired.place.under` | `0` | Allows wired furniture placement under other items. | +| `wired.tick.interval.ms` | `50` | Global tick interval in milliseconds for repeater-style wired. | +| `wired.tick.resolution` | `100` | Legacy compatibility tick resolution value. | +| `wired.tick.debug` | `0` | Enables verbose logging for the tick service. | +| `wired.tick.thread.priority` | `6` | Java thread priority for the tick service. | +| `wired.highscores.displaycount` | `25` | Maximum wired highscore entries shown to the user. | +| `wired.abuse.max.recursion.depth` | `10` | Maximum recursive wired depth before execution stops. | +| `wired.abuse.max.events.per.window` | `100` | Maximum identical events allowed inside the abuse rate-limit window. | +| `wired.abuse.rate.limit.window.ms` | `10000` | Time window in milliseconds used by the abuse limiter. | +| `wired.abuse.ban.duration.ms` | `600000` | Room wired-ban duration in milliseconds after abuse detection. | +| `wired.monitor.usage.window.ms` | `1000` | Rolling window size used to calculate monitor usage. | +| `wired.monitor.usage.limit` | `1000` | Maximum usage budget allowed in one monitor window. | +| `wired.monitor.delayed.events.limit` | `100` | Maximum delayed wired events that may be pending in one room. | +| `wired.monitor.overload.average.ms` | `50` | Average execution threshold in milliseconds for overload tracking. | +| `wired.monitor.overload.peak.ms` | `150` | Peak execution threshold in milliseconds for overload tracking. | +| `wired.monitor.overload.consecutive.windows` | `2` | Consecutive overloaded windows required before `EXECUTOR_OVERLOAD`. | +| `wired.monitor.heavy.usage.percent` | `70` | Usage percentage threshold that contributes to `MARKED_AS_HEAVY`. | +| `wired.monitor.heavy.consecutive.windows` | `5` | Consecutive heavy windows required before the room is marked heavy. | +| `wired.monitor.heavy.delayed.percent` | `60` | Delayed queue percentage threshold that contributes to the heavy state. | + +--- + +## 4. Monitor tab + +## 4.1 Statistics shown in the UI + +The `Monitor` tab currently shows: + +- `Wired usage` +- `Is heavy` +- `Room furni` +- `Wall furni` +- `Delayed events` +- `Average execution` +- `Peak execution` +- `Recursion` +- `Killed remaining` +- `Permanent furni vars` + +### 4.1.1 `Wired usage` + +Format: + +```text +usageCurrentWindow / usageLimitPerWindow +``` + +Source: + +- server-side `WiredRoomDiagnostics` + +Meaning: + +- `usageCurrentWindow` = cost consumed in the current rolling monitor window +- `usageLimitPerWindow` = max allowed budget before `EXECUTION_CAP` + +### 4.1.2 `Is heavy` + +Format: + +```text +Yes / No +``` + +Source: + +- server-side boolean from `WiredRoomDiagnostics` + +Meaning: + +- `Yes` if the room has crossed the heavy thresholds for enough consecutive windows + +### 4.1.3 `Room furni` + +Format: + +```text +(floor count + wall count) / roomItemLimit +``` + +Source: + +- numerator: renderer room object counts +- denominator: server room item limit exposed in room/session data + +### 4.1.4 `Wall furni` + +Format: + +```text +wall count / roomItemLimit +``` + +Important note: + +- there is **no separate wall-only cap** here +- the denominator is the same room furni limit exposed by the server + +### 4.1.5 `Delayed events` + +Format: + +```text +delayedEventsPending / delayedEventsLimit +``` + +Source: + +- server-side `WiredRoomDiagnostics` + +### 4.1.6 `Average execution` + +Format: + +```text +averageExecutionMs + "ms" +``` + +Meaning: + +- average execution time of sampled stacks inside the current monitor window + +### 4.1.7 `Peak execution` + +Format: + +```text +peakExecutionMs + "ms" +``` + +Meaning: + +- highest sampled execution time inside the current monitor window + +### 4.1.8 `Recursion` + +Format: + +```text +recursionDepthCurrent / recursionDepthLimit +``` + +Meaning: + +- current nested wired call depth vs the configured recursion cap + +### 4.1.9 `Killed remaining` + +Format: + +```text +killedRemainingSeconds + "s" +``` + +Meaning: + +- remaining room cooldown while wired execution is temporarily halted by protection logic + +### 4.1.10 `Permanent furni vars` + +Format: + +```text +customVariableEntryCount / 60 +``` + +Current meaning: + +- the numerator is the total number of renderer-side entries stored inside `RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES` +- the denominator `60` is currently a fixed UI denominator + +This is currently **renderer-side custom variable count**, not a DB row count. + +--- + +## 4.2 Cost model behind `Wired usage` + +The current estimated stack cost is computed in the emulator. + +### 4.2.1 Base formula + +```text +cost = 1 +cost += number_of_conditions +cost += 2 for each selector effect +cost += 3 for each non-selector effect +cost += 4 extra for each delayed effect +cost += recursionDepth * 2 +cost = max(1, cost) +``` + +### 4.2.2 Practical breakdown + +| Element | Cost | +|---|---:| +| Base stack cost | `1` | +| Each condition | `+1` | +| Each selector effect | `+2` | +| Each regular effect | `+3` | +| Each delayed effect | `+4` extra | +| Each recursion level | `+2` | + +### 4.2.3 Example + +If a stack has: + +- `2` conditions +- `1` selector +- `2` normal effects +- `1` delayed effect +- recursion depth `1` + +Then: + +```text +1 ++ 2 conditions ++ 2 selector ++ 6 regular effects ++ 4 delayed effect extra ++ 2 recursion += 17 +``` + +That `17` is what is attempted against: + +```text +usageCurrentWindow + estimatedCost <= usageLimitPerWindow +``` + +If the result would exceed the budget, the engine records `EXECUTION_CAP`. + +--- + +## 4.3 Heavy / overload calculations + +### 4.3.1 Overload + +A monitor window is considered overloaded when: + +```text +executionSamplesCurrentWindow > 0 +AND ( + averageExecutionMs >= overloadAverageThresholdMs + OR + peakExecutionMs >= overloadPeakThresholdMs +) +``` + +After `wired.monitor.overload.consecutive.windows` consecutive overloaded windows: + +- the room logs `EXECUTOR_OVERLOAD` + +### 4.3.2 Heavy + +A monitor window is considered heavy when at least one of these is true: + +```text +usagePercent >= heavyUsageThresholdPercent +OR +delayedPercent >= heavyDelayedThresholdPercent +OR +overloadWindow == true +``` + +Where: + +```text +usagePercent = round(usageCurrentWindow * 100 / usageLimitPerWindow) +delayedPercent = round(delayedEventsPending * 100 / delayedEventsLimit) +``` + +After `wired.monitor.heavy.consecutive.windows` consecutive heavy windows: + +- the room is marked heavy +- the monitor logs `MARKED_AS_HEAVY` + +--- + +## 4.4 Error / warning log types + +The monitor currently supports: + +- `EXECUTION_CAP` +- `DELAYED_EVENTS_CAP` +- `EXECUTOR_OVERLOAD` +- `MARKED_AS_HEAVY` +- `KILLED` +- `RECURSION_TIMEOUT` + +Each log/history entry can carry: + +- type +- severity +- amount/count +- latest occurrence +- reason/motivation +- trigger/source label +- trigger/source id + +--- + +## 5. Inspection tab + +## 5.1 Furni inspection + +Current variables include: + +- `@id` +- `@class_id` +- `@height` +- `@state` +- `@position.x` +- `@position.y` +- `@rotation` +- `@altitude` +- `@wallitem_offset` (wall items only) +- `@type` +- `@dimensions.x` +- `@dimensions.y` +- `@owner_id` +- dynamic flags: + - `@can_sit_on` + - `@can_lay_on` + - `@can_stand_on` + - `@is_stackable` +- extra teleport variable when relevant: + - `~teleport.target_id` + +Editable fields: + +- `@state` +- `@position.x` +- `@position.y` +- `@rotation` +- `@altitude` +- `@wallitem_offset` (wall items) + +Important notes: + +- floor moves are sent through wired-style movement flow/animation +- wall item updates use wall position recomposition +- booleans such as sit/lay/stand/stack come from `items_base`-derived metadata, not from `FurnitureData.json` + +## 5.2 User inspection + +Current variables include: + +- `@index` +- `@type` +- `@gender` +- `@level` +- `@achievement_score` +- `@position.x` +- `@position.y` +- `@direction` +- `@altitude` +- `@favourite_group_id` +- `@room_entry` +- `@room_entry.teleport_id` +- `@user_id` / `@bot_id` / `@pet_id` + +Dynamic flags/actions include: + +- `@is_hc` +- `@has_rights` +- `@is_owner` +- `@is_group_admin` +- `@is_mute` +- `@is_trading` +- `@is_frozen` +- `@effect` +- `@team_score` +- `@team_color` +- `@team_type` +- `@sign` +- `@dance` +- `@is_idle` +- `@handitems` + +Editable fields: + +- `@position.x` +- `@position.y` +- `@direction` + +## 5.3 Global inspection + +Current variables include: + +- `@furni_count` +- `@user_count` +- `@wired_timer` +- `@teams.red.score` +- `@teams.green.score` +- `@teams.blue.score` +- `@teams.yellow.score` +- `@teams.red.size` +- `@teams.green.size` +- `@teams.blue.size` +- `@teams.yellow.size` +- `@room_id` +- `@group_id` +- `@timezone_server` +- `@timezone_client` +- `@current_time` +- `@current_time.millisecond_of_second` +- `@current_time.seconds_of_minute` +- `@current_time.minute_of_hour` +- `@current_time.hour_of_day` +- `@current_time.day_of_week` +- `@current_time.day_of_month` +- `@current_time.day_of_year` +- `@current_time.week_of_year` +- `@current_time.month_of_year` +- `@current_time.year` + +Important notes: + +- `@timezone_server` comes from the emulator room/session snapshot and follows `hotel.timezone` +- `@timezone_client` comes from the browser +- `@wired_timer` is currently client-side time since room entry +- `@current_time.*` is currently based on the server hotel time snapshot plus client-side progression + +--- + +## 6. UI behaviour notes + +### 6.1 Monitor windows + +The monitor now uses real Nitro card windows for: + +- info +- log history +- error information + +This means they are: + +- closable with the standard Nitro card close button +- draggable +- resizable + +### 6.2 Keep selected + +In `Inspection`: + +- when `Keep selected` is enabled +- clicking another furni/user does **not** replace the current preview/selection + +### 6.3 Inline editing + +Inline editors: + +- can be opened by clicking the row +- submit on `Enter` +- stop accidental room chat typing while the input is focused + +--- + +## 7. Current limitations + +- `Permanent furni vars` currently uses a fixed denominator (`60`) in the Nitro UI +- `@wired_timer` is still client-side, not a dedicated server timer +- `wired.tick.resolution` is kept for compatibility/documentation, but the current tick service uses `wired.tick.interval.ms` +- `wired.highscores.displaycount` is migrated/documented, but its usage should be validated in the current runtime path if highscore behaviour is changed later From 71e3878e53d749fa824bbfa3277f63d7c781837d Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Fri, 3 Apr 2026 05:22:25 +0200 Subject: [PATCH 2/8] chore: checkpoint current work --- .../effects/WiredEffectMoveRotateUser.java | 24 +--- .../wired/effects/WiredEffectUserToFurni.java | 8 +- .../WiredExtraVariableTextConnector.java | 8 ++ .../habbohotel/rooms/RoomItemManager.java | 6 +- .../wired/core/WiredMoveCarryHelper.java | 8 ++ .../wired/core/WiredUserMovementHelper.java | 122 +++++++++++++++--- .../com/eu/habbo/messages/PacketManager.java | 2 + .../eu/habbo/messages/incoming/Incoming.java | 1 + .../rooms/users/RoomUserWalkEvent.java | 13 +- .../wired/WiredUserInspectMoveEvent.java | 83 ++++++++++++ .../outgoing/rooms/RoomDataComposer.java | 14 +- 11 files changed, 244 insertions(+), 45 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserInspectMoveEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java index 6be0ff53..50e9b916 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java @@ -15,6 +15,7 @@ import com.eu.habbo.habbohotel.rooms.RoomUserRotation; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics; import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; @@ -53,6 +54,7 @@ public class WiredEffectMoveRotateUser extends InteractionWiredEffect { @Override public void execute(WiredContext ctx) { Room room = ctx.room(); + WiredMovementPhysics movementPhysics = WiredMoveCarryHelper.getUserMovementPhysics(room, this, ctx); for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { if (roomUnit == null || roomUnit.getRoom() != room) { @@ -63,7 +65,7 @@ public class WiredEffectMoveRotateUser extends InteractionWiredEffect { RoomUserRotation targetBodyRotation = hasRotation ? this.getTargetRotation(roomUnit) : roomUnit.getBodyRotation(); RoomUserRotation targetHeadRotation = hasRotation ? targetBodyRotation : roomUnit.getHeadRotation(); RoomTile targetTile = (this.movementDirection >= 0) ? this.getTargetTile(room, roomUnit, this.movementDirection) : null; - boolean canMove = this.canMoveTo(room, roomUnit, targetTile); + boolean canMove = this.canMoveTo(room, roomUnit, targetTile, movementPhysics); boolean noAnimation = WiredMoveCarryHelper.hasNoAnimationExtra(room, this); int animationDuration = noAnimation ? 0 : WiredMoveCarryHelper.getAnimationDuration(room, this, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION); int activeWindowMs = this.resolveActiveWindow(canMove, hasRotation, noAnimation, animationDuration); @@ -72,7 +74,7 @@ public class WiredEffectMoveRotateUser extends InteractionWiredEffect { double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); this.markActive(roomUnit, activeWindowMs); if (!WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, targetBodyRotation, targetHeadRotation, - animationDuration, noAnimation)) { + animationDuration, noAnimation, movementPhysics)) { if (hasRotation) { WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetBodyRotation, targetHeadRotation); } @@ -266,22 +268,8 @@ public class WiredEffectMoveRotateUser extends InteractionWiredEffect { return room.getLayout().getTile((short) (currentTile.x + deltaX), (short) (currentTile.y + deltaY)); } - private boolean canMoveTo(Room room, RoomUnit roomUnit, RoomTile targetTile) { - if (targetTile == null || targetTile.state == RoomTileState.INVALID || targetTile.state == RoomTileState.BLOCKED) { - return false; - } - - if (!room.tileWalkable(targetTile)) { - return false; - } - - for (RoomUnit unit : room.getRoomUnitsAt(targetTile)) { - if (unit != null && unit != roomUnit) { - return false; - } - } - - return true; + private boolean canMoveTo(Room room, RoomUnit roomUnit, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + return WiredUserMovementHelper.canMoveTo(room, roomUnit, targetTile, movementPhysics); } private void markActive(RoomUnit roomUnit, int durationMs) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserToFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserToFurni.java index 2d2491d5..69960024 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserToFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserToFurni.java @@ -14,6 +14,7 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics; import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; @@ -47,6 +48,7 @@ public class WiredEffectUserToFurni extends WiredEffectUserFurniBase { public void execute(WiredContext ctx) { Room room = ctx.room(); HabboItem item = this.resolveLastItem(ctx); + WiredMovementPhysics movementPhysics = WiredMoveCarryHelper.getUserMovementPhysics(room, this, ctx); if (room == null || item == null) { return; @@ -58,7 +60,7 @@ public class WiredEffectUserToFurni extends WiredEffectUserFurniBase { } for (Habbo habbo : this.resolveHabbos(room, ctx)) { - this.moveHabboSmooth(room, habbo, item, targetTile); + this.moveHabboSmooth(room, habbo, item, targetTile, movementPhysics); } } @@ -227,7 +229,7 @@ public class WiredEffectUserToFurni extends WiredEffectUserFurniBase { return true; } - private void moveHabboSmooth(Room room, Habbo habbo, HabboItem item, RoomTile targetTile) { + private void moveHabboSmooth(Room room, Habbo habbo, HabboItem item, RoomTile targetTile, WiredMovementPhysics movementPhysics) { if (room == null || habbo == null || item == null || targetTile == null || habbo.getRoomUnit() == null) { return; } @@ -245,7 +247,7 @@ public class WiredEffectUserToFurni extends WiredEffectUserFurniBase { double newZ = item.getZ() + Item.getCurrentHeight(item); int animationDuration = noAnimation ? 0 : WiredMoveCarryHelper.getAnimationDuration(room, this, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION); if (!WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, newZ, - roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), animationDuration, noAnimation)) { + roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), animationDuration, noAnimation, movementPhysics)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java index eb419c03..388f30a4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java @@ -1,11 +1,13 @@ package com.eu.habbo.habbohotel.items.interactions.wired.extra; +import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.ServerMessage; @@ -38,6 +40,12 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { @Override public boolean saveData(WiredSettings settings, GameClient gameClient) { this.setMappingsText(settings.getStringParam()); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room != null) { + WiredContextVariableSupport.broadcastDefinitions(room); + } + return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index a52b9c3e..a95a5cd7 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -21,6 +21,7 @@ import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVari import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable; import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal; @@ -807,6 +808,7 @@ public class RoomItemManager { isWiredItem = true; } else if (item instanceof InteractionWiredExtra) { boolean removedContextDefinition = false; + boolean removedVariableTextConnector = false; if (item instanceof WiredExtraUserVariable) { this.room.getUserVariableManager().removeDefinition(item.getId()); } else if (item instanceof WiredExtraFurniVariable) { @@ -815,6 +817,8 @@ public class RoomItemManager { this.room.getRoomVariableManager().removeDefinition(item.getId()); } else if (item instanceof WiredExtraContextVariable) { removedContextDefinition = true; + } else if (item instanceof WiredExtraVariableTextConnector) { + removedVariableTextConnector = true; } else if (item instanceof WiredExtraVariableReference) { if (((WiredExtraVariableReference) item).isRoomReference()) { this.room.getRoomVariableManager().removeDefinition(item.getId()); @@ -833,7 +837,7 @@ public class RoomItemManager { } } specialTypes.removeExtra((InteractionWiredExtra) item); - if (removedContextDefinition) { + if (removedContextDefinition || removedVariableTextConnector) { WiredContextVariableSupport.broadcastDefinitions(this.room); } isWiredItem = true; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java index 49e14b04..5647a45e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java @@ -404,6 +404,14 @@ public final class WiredMoveCarryHelper { return (extra != null) ? extra.getDurationMs() : fallbackDuration; } + public static WiredMovementPhysics getUserMovementPhysics(Room room, HabboItem stackItem, WiredContext ctx) { + if (room == null || stackItem == null) { + return WiredMovementPhysics.NONE; + } + + return getMovementPhysics(room, stackItem, null, ctx); + } + public static int resolveMoveStepElapsed(RoomUnit roomUnit) { if (roomUnit == null) { return 0; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java index 9777db22..2ead572e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -36,20 +37,26 @@ public final class WiredUserMovementHelper { public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, int duration) { return moveUser(room, roomUnit, targetTile, targetZ, roomUnit == null ? null : roomUnit.getBodyRotation(), - roomUnit == null ? null : roomUnit.getHeadRotation(), duration, false); + roomUnit == null ? null : roomUnit.getHeadRotation(), duration, false, WiredMovementPhysics.NONE); } public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, int duration) { - return moveUser(room, roomUnit, targetTile, targetZ, bodyRotation, headRotation, duration, false); + return moveUser(room, roomUnit, targetTile, targetZ, bodyRotation, headRotation, duration, false, WiredMovementPhysics.NONE); } public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, int duration, boolean noAnimation) { + return moveUser(room, roomUnit, targetTile, targetZ, bodyRotation, headRotation, duration, noAnimation, WiredMovementPhysics.NONE); + } + + public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, int duration, boolean noAnimation, WiredMovementPhysics movementPhysics) { if (room == null || roomUnit == null || targetTile == null || room.getLayout() == null) { return false; } RoomTile oldLocation = roomUnit.getCurrentLocation(); - if (oldLocation == null || hasBlockingUnits(room, roomUnit, targetTile)) { + WiredMovementPhysics resolvedMovementPhysics = movementPhysics == null ? WiredMovementPhysics.NONE : movementPhysics; + + if (oldLocation == null || !canMoveTo(room, roomUnit, targetTile, resolvedMovementPhysics)) { return false; } @@ -66,13 +73,12 @@ public final class WiredUserMovementHelper { } runWithSuppressedStatusUpdates(Collections.singletonList(roomUnit), () -> { - roomUnit.setPreviousLocation(oldLocation); - roomUnit.setCurrentLocation(targetTile); roomUnit.removeStatus(RoomUnitStatus.MOVE); roomUnit.setZ(targetZ); + roomUnit.setLocation(targetTile); + roomUnit.setPath(new LinkedList<>()); roomUnit.setBodyRotation(resolvedBodyRotation); roomUnit.setHeadRotation(resolvedHeadRotation); - roomUnit.stopWalking(); roomUnit.resetIdleTimer(); if (habbo != null) { @@ -99,10 +105,8 @@ public final class WiredUserMovementHelper { suppressStatusComposer(roomUnit, animationDuration); room.sendComposer(new WiredMovementsComposer(movements).compose()); - roomUnit.setPreviousLocation(targetTile); - roomUnit.setPreviousLocationZ(roomUnit.getZ()); - scheduleTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem, animationDuration); + scheduleFinalStatusSync(room, roomUnit, targetTile, animationDuration); schedulePostureSync(room, roomUnit, targetTile, animationDuration); return true; } @@ -161,7 +165,32 @@ public final class WiredUserMovementHelper { SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId()); } - private static boolean hasBlockingUnits(Room room, RoomUnit roomUnit, RoomTile targetTile) { + public static boolean canMoveTo(Room room, RoomUnit roomUnit, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + if (room == null || roomUnit == null || targetTile == null) { + return false; + } + + WiredMovementPhysics resolvedMovementPhysics = movementPhysics == null ? WiredMovementPhysics.NONE : movementPhysics; + + if (targetTile.state == null || targetTile.state == com.eu.habbo.habbohotel.rooms.RoomTileState.INVALID) { + return false; + } + + if (targetTile.state == com.eu.habbo.habbohotel.rooms.RoomTileState.BLOCKED + && !canBypassBlockedTile(room, targetTile, resolvedMovementPhysics)) { + return false; + } + + if (!room.getLayout().tileWalkable(targetTile.x, targetTile.y) + && !room.canSitOrLayAt(targetTile.x, targetTile.y) + && !canBypassBlockedTile(room, targetTile, resolvedMovementPhysics)) { + return false; + } + + return !hasBlockingUnits(room, roomUnit, targetTile, resolvedMovementPhysics); + } + + private static boolean hasBlockingUnits(Room room, RoomUnit roomUnit, RoomTile targetTile, WiredMovementPhysics movementPhysics) { Collection units = room.getRoomUnitsAt(targetTile); if (units == null || units.isEmpty()) { @@ -169,7 +198,9 @@ public final class WiredUserMovementHelper { } for (RoomUnit targetUnit : units) { - if (targetUnit != null && targetUnit != roomUnit) { + if (targetUnit != null + && targetUnit != roomUnit + && !movementPhysics.shouldIgnoreUser(targetUnit)) { return true; } } @@ -177,15 +208,50 @@ public final class WiredUserMovementHelper { return false; } + private static boolean canBypassBlockedTile(Room room, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + if (room == null || targetTile == null || movementPhysics == null || !movementPhysics.isActive()) { + return false; + } + + Collection items = room.getItemsAt(targetTile); + if (items == null || items.isEmpty()) { + return false; + } + + boolean hasIgnoredFurni = false; + + for (HabboItem item : items) { + if (item == null) { + continue; + } + + if (movementPhysics.isBlockingFurni(item)) { + return false; + } + + if (movementPhysics.shouldIgnoreFurni(item)) { + hasIgnoredFurni = true; + continue; + } + + if (!item.isWalkable() + && !item.getBaseItem().allowSit() + && !item.getBaseItem().allowLay()) { + return false; + } + } + + return hasIgnoredFurni; + } + private static boolean moveUserInstant(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, RoomTile oldLocation, HabboItem oldTopItem, HabboItem newTopItem, Habbo habbo) { runWithSuppressedStatusUpdates(Collections.singletonList(roomUnit), () -> { - roomUnit.setPreviousLocation(oldLocation); - roomUnit.setCurrentLocation(targetTile); roomUnit.removeStatus(RoomUnitStatus.MOVE); roomUnit.setZ(targetZ); + roomUnit.setLocation(targetTile); + roomUnit.setPath(new LinkedList<>()); roomUnit.setBodyRotation(bodyRotation); roomUnit.setHeadRotation(headRotation); - roomUnit.stopWalking(); roomUnit.resetIdleTimer(); if (habbo != null) { @@ -193,13 +259,12 @@ public final class WiredUserMovementHelper { movedHabbos.add(habbo); room.updateHabbosAt(targetTile.x, targetTile.y, movedHabbos); } - - roomUnit.setPreviousLocation(targetTile); - roomUnit.setPreviousLocationZ(roomUnit.getZ()); roomUnit.statusUpdate(false); }); processTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem); + roomUnit.setPreviousLocation(roomUnit.getCurrentLocation()); + roomUnit.setPreviousLocationZ(roomUnit.getZ()); room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); return true; } @@ -258,6 +323,29 @@ public final class WiredUserMovementHelper { }, delay + STATUS_SUPPRESSION_GRACE_MS + 25); } + private static void scheduleFinalStatusSync(Room room, RoomUnit roomUnit, RoomTile targetTile, int delay) { + if (room == null || roomUnit == null || targetTile == null) { + return; + } + + Emulator.getThreading().run(() -> { + if (room == null || !room.isLoaded() || roomUnit == null || roomUnit.getCurrentLocation() == null) { + return; + } + + if (roomUnit.isWalking() + || roomUnit.getCurrentLocation().x != targetTile.x + || roomUnit.getCurrentLocation().y != targetTile.y) { + return; + } + + clearStatusComposerSuppression(roomUnit); + roomUnit.setPreviousLocation(roomUnit.getCurrentLocation()); + roomUnit.setPreviousLocationZ(roomUnit.getZ()); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + }, Math.max(delay, 1) + 25); + } + private static void suppressStatusComposer(RoomUnit roomUnit, int duration) { if (roomUnit == null) { return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 72fe9403..c3fe35d4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -70,6 +70,7 @@ import com.eu.habbo.messages.incoming.wired.WiredEffectSaveDataEvent; import com.eu.habbo.messages.incoming.wired.WiredMonitorRequestEvent; import com.eu.habbo.messages.incoming.wired.WiredRoomSettingsRequestEvent; import com.eu.habbo.messages.incoming.wired.WiredRoomSettingsSaveEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserInspectMoveEvent; import com.eu.habbo.messages.incoming.wired.WiredUserVariableManageEvent; import com.eu.habbo.messages.incoming.wired.WiredUserVariableUpdateEvent; import com.eu.habbo.messages.incoming.wired.WiredUserVariablesRequestEvent; @@ -627,6 +628,7 @@ public class PacketManager { this.registerHandler(Incoming.WiredUserVariablesRequestEvent, WiredUserVariablesRequestEvent.class); this.registerHandler(Incoming.WiredUserVariableUpdateEvent, WiredUserVariableUpdateEvent.class); this.registerHandler(Incoming.WiredUserVariableManageEvent, WiredUserVariableManageEvent.class); + this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class); } void registerUnknown() throws Exception { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 4f8554cf..2c1aad0a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -415,6 +415,7 @@ public class Incoming { public static final int WiredUserVariablesRequestEvent = 10024; public static final int WiredUserVariableUpdateEvent = 10025; public static final int WiredUserVariableManageEvent = 10026; + public static final int WiredUserInspectMoveEvent = 10027; public static final int RequestInventoryPetDelete = 10030; public static final int RequestInventoryBadgeDelete = 10031; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java index e54fc6e7..26f499c6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java @@ -120,8 +120,19 @@ public class RoomUserWalkEvent extends MessageHandler { roomUnit.getMoveBlockingTask().get(); } - if (WiredUserMovementHelper.shouldSuppressStatusComposer(roomUnit)) { + boolean needsLocationResync = + roomUnit.getCurrentLocation() != null + && (roomUnit.getPreviousLocation() == null + || roomUnit.getPreviousLocation().x != roomUnit.getCurrentLocation().x + || roomUnit.getPreviousLocation().y != roomUnit.getCurrentLocation().y + || Math.abs(roomUnit.getPreviousLocationZ() - roomUnit.getZ()) > 0.01D); + + if (WiredUserMovementHelper.shouldSuppressStatusComposer(roomUnit) || needsLocationResync) { WiredUserMovementHelper.clearStatusComposerSuppression(roomUnit); + if (roomUnit.getCurrentLocation() != null) { + roomUnit.setPreviousLocation(roomUnit.getCurrentLocation()); + roomUnit.setPreviousLocationZ(roomUnit.getZ()); + } room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserInspectMoveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserInspectMoveEvent.java new file mode 100644 index 00000000..fd159b9e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserInspectMoveEvent.java @@ -0,0 +1,83 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserInspectMoveEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canModifyWired(this.client.getHabbo())) { + return; + } + + if (this.packet.bytesAvailable() < 16) { + return; + } + + int roomUnitId = this.packet.readInt(); + int x = this.packet.readInt(); + int y = this.packet.readInt(); + int direction = this.packet.readInt(); + + RoomUnit roomUnit = resolveRoomUnit(room, roomUnitId); + + if (roomUnit == null || roomUnit.getCurrentLocation() == null || room.getLayout() == null) { + return; + } + + RoomUserRotation targetRotation = RoomUserRotation.fromValue((((direction % 8) + 8) % 8)); + boolean positionChanged = roomUnit.getX() != x || roomUnit.getY() != y; + boolean directionChanged = roomUnit.getBodyRotation() != targetRotation || roomUnit.getHeadRotation() != targetRotation; + + if (!positionChanged) { + if (directionChanged) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetRotation, targetRotation); + } + + return; + } + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + + if (targetTile == null || targetTile.state == RoomTileState.INVALID || targetTile.state == RoomTileState.BLOCKED) { + return; + } + + double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); + + if (!WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, targetRotation, targetRotation, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, false) + && directionChanged) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetRotation, targetRotation); + } + } + + @Override + public int getRatelimit() { + return 100; + } + + private RoomUnit resolveRoomUnit(Room room, int roomUnitId) { + if (room == null || roomUnitId <= 0) { + return null; + } + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (roomUnit != null && roomUnit.getId() == roomUnitId) { + return roomUnit; + } + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java index 4f6163bc..50770114 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java @@ -3,6 +3,7 @@ package com.eu.habbo.messages.outgoing.rooms; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomPromotion; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; @@ -24,6 +25,9 @@ public class RoomDataComposer extends MessageComposer { @Override protected ServerMessage composeInternal() { + final RoomPromotion promotion = this.room.getPromotion(); + final boolean hasPromotion = this.room.isPromoted() && (promotion != null); + this.response.init(Outgoing.RoomDataComposer); this.response.appendBoolean(this.enterRoom); this.response.appendInt(this.room.getId()); @@ -64,7 +68,7 @@ public class RoomDataComposer extends MessageComposer { base = base | 8; } - if (this.room.isPromoted()) { + if (hasPromotion) { base = base | 4; } @@ -87,10 +91,10 @@ public class RoomDataComposer extends MessageComposer { } } - if (this.room.isPromoted()) { - this.response.appendString(this.room.getPromotion().getTitle()); - this.response.appendString(this.room.getPromotion().getDescription()); - this.response.appendInt((this.room.getPromotion().getEndTimestamp() - Emulator.getIntUnixTimestamp()) / 60); + if (hasPromotion) { + this.response.appendString(promotion.getTitle()); + this.response.appendString(promotion.getDescription()); + this.response.appendInt((promotion.getEndTimestamp() - Emulator.getIntUnixTimestamp()) / 60); } this.response.appendBoolean(this.roomForward); From 6ab152c47df76743e3131f97cb37fad57be8ce6f Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Fri, 3 Apr 2026 12:09:16 +0200 Subject: [PATCH 3/8] feat: add room control furni and stack walk helper --- .../habbo/habbohotel/items/ItemManager.java | 8 + .../InteractionAreaHideControl.java | 172 ++++++++++++++++ .../InteractionConfInvisControl.java | 16 ++ .../InteractionHanditemBlockControl.java | 16 ++ .../InteractionQueueSpeedControl.java | 186 ++++++++++++++++++ .../InteractionRemoteSwitchControl.java | 21 ++ .../InteractionStackWalkHelper.java | 48 +++++ .../InteractionWiredDisableControl.java | 16 ++ .../com/eu/habbo/habbohotel/rooms/Room.java | 53 +++-- .../habbohotel/rooms/RoomAreaHideSupport.java | 100 ++++++++++ .../rooms/RoomConfInvisSupport.java | 107 ++++++++++ .../habbohotel/rooms/RoomCycleManager.java | 3 +- .../rooms/RoomHanditemBlockSupport.java | 65 ++++++ .../habbohotel/rooms/RoomItemManager.java | 157 ++++++++++++--- .../habbo/habbohotel/rooms/RoomManager.java | 6 + .../rooms/RoomQueueSpeedControlSupport.java | 77 ++++++++ .../habbohotel/rooms/RoomRollerManager.java | 2 +- .../habbohotel/rooms/RoomTileManager.java | 15 +- .../eu/habbo/habbohotel/rooms/RoomUnit.java | 3 +- .../rooms/RoomWiredDisableSupport.java | 47 +++++ .../habbo/habbohotel/wired/WiredHandler.java | 10 + .../habbohotel/wired/core/WiredManager.java | 9 + .../rooms/items/RoomPlaceItemEvent.java | 1 + .../items/SetStackHelperHeightEvent.java | 5 +- .../users/RoomUserGiveHandItemEvent.java | 5 + .../eu/habbo/messages/outgoing/Outgoing.java | 3 + .../rooms/items/AddFloorItemComposer.java | 8 +- .../rooms/items/AreaHideComposer.java | 30 +++ .../rooms/items/ConfInvisStateComposer.java | 34 ++++ .../rooms/items/FloorItemUpdateComposer.java | 9 +- .../items/HanditemBlockStateComposer.java | 25 +++ .../rooms/items/RoomFloorItemsComposer.java | 8 +- .../rooms/users/RoomUnitOnRollerComposer.java | 5 +- .../runnables/HabboGiveHandItemToHabbo.java | 8 + 34 files changed, 1217 insertions(+), 61 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionAreaHideControl.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionConfInvisControl.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionHanditemBlockControl.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionQueueSpeedControl.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRemoteSwitchControl.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionStackWalkHelper.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredDisableControl.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomAreaHideSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomConfInvisSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomHanditemBlockSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomQueueSpeedControlSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomWiredDisableSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AreaHideComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/ConfInvisStateComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/HanditemBlockStateComposer.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java index 411a530b..2951ca56 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java @@ -169,6 +169,7 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("monsterplant_seed", InteractionMonsterPlantSeed.class)); this.interactionsList.add(new ItemInteraction("gift", InteractionGift.class)); this.interactionsList.add(new ItemInteraction("stack_helper", InteractionStackHelper.class)); + this.interactionsList.add(new ItemInteraction("stack_walk_helper", InteractionStackWalkHelper.class)); this.interactionsList.add(new ItemInteraction("puzzle_box", InteractionPuzzleBox.class)); this.interactionsList.add(new ItemInteraction("hopper", InteractionHopper.class)); this.interactionsList.add(new ItemInteraction("costume_hopper", InteractionCostumeHopper.class)); @@ -198,6 +199,13 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("youtube", InteractionYoutubeTV.class)); this.interactionsList.add(new ItemInteraction("jukebox", InteractionJukeBox.class)); this.interactionsList.add(new ItemInteraction("switch", InteractionSwitch.class)); + this.interactionsList.add(new ItemInteraction("conf_invis_control", InteractionConfInvisControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_invis_control", InteractionConfInvisControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_area_hide", InteractionAreaHideControl.class)); + this.interactionsList.add(new ItemInteraction("conf_area_hide", InteractionAreaHideControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_handitem_block", InteractionHanditemBlockControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_queue_speed", InteractionQueueSpeedControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_wired_disable", InteractionWiredDisableControl.class)); this.interactionsList.add(new ItemInteraction("switch_remote_control", InteractionSwitchRemoteControl.class)); this.interactionsList.add(new ItemInteraction("fx_box", InteractionFXBox.class)); this.interactionsList.add(new ItemInteraction("blackhole", InteractionBlackHole.class)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionAreaHideControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionAreaHideControl.java new file mode 100644 index 00000000..5c7e975b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionAreaHideControl.java @@ -0,0 +1,172 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomAreaHideSupport; +import com.eu.habbo.habbohotel.rooms.RoomLayout; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.map.hash.THashMap; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionAreaHideControl extends InteractionCustomValues { + public static final THashMap defaultValues = new THashMap() { + { + this.put("state", "0"); + } + { + this.put("rootX", "0"); + } + { + this.put("rootY", "0"); + } + { + this.put("width", "0"); + } + { + this.put("length", "0"); + } + { + this.put("invisibility", "0"); + } + { + this.put("wallItems", "0"); + } + { + this.put("invert", "0"); + } + }; + + public InteractionAreaHideControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem, defaultValues); + this.normalizeValues(); + } + + public InteractionAreaHideControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells, defaultValues); + this.normalizeValues(); + } + + @Override + public void serializeExtradata(ServerMessage serverMessage) { + this.normalizeValues(); + + serverMessage.appendInt(5 + (this.isLimited() ? 256 : 0)); + serverMessage.appendInt(8); + serverMessage.appendInt(RoomAreaHideSupport.getState(this)); + serverMessage.appendInt(RoomAreaHideSupport.getRootX(this)); + serverMessage.appendInt(RoomAreaHideSupport.getRootY(this)); + serverMessage.appendInt(RoomAreaHideSupport.getWidth(this)); + serverMessage.appendInt(RoomAreaHideSupport.getLength(this)); + serverMessage.appendInt(RoomAreaHideSupport.isInvisibilityEnabled(this) ? 1 : 0); + serverMessage.appendInt(RoomAreaHideSupport.includesWallItems(this) ? 1 : 0); + serverMessage.appendInt(RoomAreaHideSupport.isInverted(this) ? 1 : 0); + + if (this.isLimited()) { + serverMessage.appendInt(this.getLimitedSells()); + serverMessage.appendInt(this.getLimitedStack()); + } + } + + @Override + public boolean isUsable() { + return true; + } + + @Override + public boolean allowWiredResetState() { + return true; + } + + @Override + public void onClick(GameClient client, Room room, Object[] objects) throws Exception { + if (room == null) { + return; + } + + boolean wiredToggle = objects != null + && objects.length >= 2 + && objects[1] instanceof WiredEffectType; + + if (!wiredToggle) { + if (client == null || !this.canToggle(client.getHabbo(), room)) { + return; + } + } + + this.values.put("state", (RoomAreaHideSupport.getState(this) == 1) ? "0" : "1"); + this.normalizeValues(); + this.needsUpdate(true); + Emulator.getThreading().run(this); + room.updateItem(this); + } + + @Override + public void onCustomValuesSaved(Room room, GameClient client, THashMap oldValues) { + this.normalizeValues(); + } + + public boolean canToggle(Habbo habbo, Room room) { + if (habbo == null || room == null) { + return false; + } + + if (room.hasRights(habbo)) { + return true; + } + + if (!habbo.getHabboStats().isRentingSpace()) { + return false; + } + + HabboItem rentedItem = room.getHabboItem(habbo.getHabboStats().rentedItemId); + + return room.getLayout() != null + && rentedItem != null + && RoomLayout.squareInSquare( + RoomLayout.getRectangle( + rentedItem.getX(), + rentedItem.getY(), + rentedItem.getBaseItem().getWidth(), + rentedItem.getBaseItem().getLength(), + rentedItem.getRotation() + ), + RoomLayout.getRectangle( + this.getX(), + this.getY(), + this.getBaseItem().getWidth(), + this.getBaseItem().getLength(), + this.getRotation() + ) + ); + } + + private void normalizeValues() { + this.values.put("state", booleanFlag(this.values.get("state"))); + this.values.put("rootX", Integer.toString(nonNegative(this.values.get("rootX")))); + this.values.put("rootY", Integer.toString(nonNegative(this.values.get("rootY")))); + this.values.put("width", Integer.toString(nonNegative(this.values.get("width")))); + this.values.put("length", Integer.toString(nonNegative(this.values.get("length")))); + this.values.put("invisibility", booleanFlag(this.values.get("invisibility"))); + this.values.put("wallItems", booleanFlag(this.values.get("wallItems"))); + this.values.put("invert", booleanFlag(this.values.get("invert"))); + } + + private static int nonNegative(String value) { + try { + return Math.max(0, Integer.parseInt(value)); + } catch (Exception ignored) { + return 0; + } + } + + private static String booleanFlag(String value) { + return ("1".equals(value) || "true".equalsIgnoreCase(value)) ? "1" : "0"; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionConfInvisControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionConfInvisControl.java new file mode 100644 index 00000000..fe132642 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionConfInvisControl.java @@ -0,0 +1,16 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionConfInvisControl extends InteractionRemoteSwitchControl { + public InteractionConfInvisControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionConfInvisControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionHanditemBlockControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionHanditemBlockControl.java new file mode 100644 index 00000000..59362e23 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionHanditemBlockControl.java @@ -0,0 +1,16 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionHanditemBlockControl extends InteractionRemoteSwitchControl { + public InteractionHanditemBlockControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionHanditemBlockControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionQueueSpeedControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionQueueSpeedControl.java new file mode 100644 index 00000000..6707af8d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionQueueSpeedControl.java @@ -0,0 +1,186 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionQueueSpeedControl extends InteractionRemoteSwitchControl { + private static final int[] MODE_STATES = new int[]{0, 3, 6, 9}; + private static final int MODE_FRAME_COUNT = 3; + private static final int BASE_FRAME_DURATION_MS = 500; + + private transient volatile int animationRevision = 0; + private transient volatile int animationRoomId = 0; + private transient volatile int animationModeState = Integer.MIN_VALUE; + + public InteractionQueueSpeedControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionQueueSpeedControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + public static int toModeState(String extradata) { + int state = 0; + + try { + state = Integer.parseInt(extradata); + } catch (NumberFormatException ignored) { + } + + if (state >= 9) { + return 9; + } + + if (state >= 6) { + return 6; + } + + if (state >= 3) { + return 3; + } + + return 0; + } + + public static int toRollerSpeed(String extradata) { + int modeState = toModeState(extradata); + + if (modeState >= 9) { + return 3; + } + + if (modeState >= 6) { + return 2; + } + + if (modeState >= 3) { + return 1; + } + + return 0; + } + + public static int toRollerIntervalMs(String extradata) { + return BASE_FRAME_DURATION_MS * (toRollerSpeed(extradata) + 1); + } + + @Override + public void onClick(GameClient client, Room room, Object[] objects) throws Exception { + if (room == null) { + return; + } + + boolean wiredToggle = objects != null + && objects.length >= 2 + && objects[1] instanceof com.eu.habbo.habbohotel.wired.WiredEffectType; + + if (!wiredToggle) { + if (client == null) { + return; + } + + if (!this.canToggle(client.getHabbo(), room)) { + super.onClick(client, room, new Object[]{"QUEUE_SPEED_USE"}); + return; + } + } + + int nextModeState = getNextModeState(this.getExtradata()); + applyModeState(room, nextModeState, true); + + if (client != null) { + super.onClick(client, room, new Object[]{"TOGGLE_OVERRIDE"}); + } + } + + @Override + public void onPlace(Room room) { + super.onPlace(room); + this.ensureAnimationLoop(room); + } + + @Override + public void onPickUp(Room room) { + this.animationRevision++; + this.animationRoomId = 0; + this.animationModeState = Integer.MIN_VALUE; + super.onPickUp(room); + } + + public void ensureAnimationLoop(Room room) { + if (room == null || !room.isLoaded() || this.getRoomId() != room.getId()) { + return; + } + + int modeState = toModeState(this.getExtradata()); + + if (this.animationRoomId == room.getId() && this.animationModeState == modeState) { + return; + } + + applyModeState(room, modeState, false); + } + + private void applyModeState(Room room, int modeState, boolean persistSelection) { + if (room == null) { + return; + } + + this.animationRevision++; + this.animationRoomId = room.getId(); + this.animationModeState = modeState; + + this.setExtradata(Integer.toString(modeState)); + if (persistSelection) { + this.needsUpdate(true); + } + room.updateItemState(this); + + int revision = this.animationRevision; + int nextFrame = modeState + 1; + long delay = toRollerIntervalMs(Integer.toString(modeState)); + + Emulator.getThreading().run(() -> this.animateNextFrame(room, modeState, nextFrame, revision), delay); + } + + private void animateNextFrame(Room room, int modeState, int nextFrame, int revision) { + if (room == null || !room.isLoaded() || this.getRoomId() != room.getId()) { + return; + } + + if (revision != this.animationRevision || modeState != this.animationModeState) { + return; + } + + int maxFrame = modeState + (MODE_FRAME_COUNT - 1); + int frame = (nextFrame > maxFrame) ? modeState : nextFrame; + + this.setExtradata(Integer.toString(frame)); + room.updateItemState(this); + + int followingFrame = (frame >= maxFrame) ? modeState : (frame + 1); + long delay = toRollerIntervalMs(Integer.toString(modeState)); + + Emulator.getThreading().run(() -> this.animateNextFrame(room, modeState, followingFrame, revision), delay); + } + + private static int getNextModeState(String extradata) { + int currentModeState = toModeState(extradata); + + for (int index = 0; index < MODE_STATES.length; index++) { + if (MODE_STATES[index] != currentModeState) { + continue; + } + + return MODE_STATES[(index + 1) % MODE_STATES.length]; + } + + return MODE_STATES[0]; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRemoteSwitchControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRemoteSwitchControl.java new file mode 100644 index 00000000..aeb7f8f0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRemoteSwitchControl.java @@ -0,0 +1,21 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionRemoteSwitchControl extends InteractionDefault { + public InteractionRemoteSwitchControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionRemoteSwitchControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean isUsable() { + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionStackWalkHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionStackWalkHelper.java new file mode 100644 index 00000000..d6102f98 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionStackWalkHelper.java @@ -0,0 +1,48 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionStackWalkHelper extends HabboItem { + public InteractionStackWalkHelper(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionStackWalkHelper(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean canWalkOn(RoomUnit roomUnit, Room room, Object[] objects) { + return false; + } + + @Override + public boolean isWalkable() { + return false; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public void serializeExtradata(ServerMessage serverMessage) { + serverMessage.appendInt((this.isLimited() ? 256 : 0)); + serverMessage.appendString(this.getExtradata()); + + super.serializeExtradata(serverMessage); + } + + @Override + public boolean isUsable() { + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredDisableControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredDisableControl.java new file mode 100644 index 00000000..2c69ff86 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredDisableControl.java @@ -0,0 +1,16 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionWiredDisableControl extends InteractionRemoteSwitchControl { + public InteractionWiredDisableControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionWiredDisableControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index d0231b6f..87096178 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -2458,29 +2458,38 @@ public class Room implements Comparable, ISerialize, Runnable { } public void updateItem(HabboItem item) { - if (this.isLoaded()) { - if (item != null && item.getRoomId() == this.id) { - if (item.getBaseItem() != null) { - if (item.getBaseItem().getType() == FurnitureType.FLOOR) { - this.sendComposer(new FloorItemUpdateComposer(item).compose()); - this.updateTiles(this.getLayout() - .getTilesAt(this.layout.getTile(item.getX(), item.getY()), - item.getBaseItem().getWidth(), item.getBaseItem().getLength(), - item.getRotation())); - } else if (item.getBaseItem().getType() == FurnitureType.WALL) { - this.sendComposer(new WallItemUpdateComposer(item).compose()); + if (this.isLoaded()) { + if (item != null && item.getRoomId() == this.id) { + if (item.getBaseItem() != null) { + if (item.getBaseItem().getType() == FurnitureType.FLOOR) { + this.sendComposer(new FloorItemUpdateComposer(item).compose()); + this.updateTiles(this.getLayout() + .getTilesAt(this.layout.getTile(item.getX(), item.getY()), + item.getBaseItem().getWidth(), item.getBaseItem().getLength(), + item.getRotation())); + + if (RoomAreaHideSupport.isControllerItem(item)) { + RoomAreaHideSupport.sendState(this, item); + } + } else if (item.getBaseItem().getType() == FurnitureType.WALL) { + this.sendComposer(new WallItemUpdateComposer(item).compose()); + } } } - } } } public void updateItemState(HabboItem item) { - if (!item.isLimited()) { - this.sendComposer(new ItemStateComposer(item).compose()); - } else { - this.sendComposer(new FloorItemUpdateComposer(item).compose()); - } + if (item != null && RoomAreaHideSupport.isControllerItem(item)) { + this.updateItem(item); + return; + } + + if (!item.isLimited()) { + this.sendComposer(new ItemStateComposer(item).compose()); + } else { + this.sendComposer(new FloorItemUpdateComposer(item).compose()); + } if (item.getBaseItem().getType() == FurnitureType.FLOOR) { if (this.layout == null) { @@ -2495,6 +2504,16 @@ public class Room implements Comparable, ISerialize, Runnable { ((InteractionMultiHeight) item).updateUnitsOnItem(this); } } + + if (item.getBaseItem().getType() == FurnitureType.FLOOR + && (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item))) { + RoomConfInvisSupport.sendState(this); + } + + if (item.getBaseItem().getType() == FurnitureType.FLOOR + && RoomHanditemBlockSupport.isControllerItem(item)) { + RoomHanditemBlockSupport.sendState(this); + } } public int getUserFurniCount(int userId) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomAreaHideSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomAreaHideSupport.java new file mode 100644 index 00000000..426cbf4c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomAreaHideSupport.java @@ -0,0 +1,100 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.interactions.InteractionAreaHideControl; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.outgoing.rooms.items.AreaHideComposer; + +public final class RoomAreaHideSupport { + private RoomAreaHideSupport() { + } + + public static boolean isControllerItem(HabboItem item) { + return item instanceof InteractionAreaHideControl + || hasInteractionName(item, "wf_conf_area_hide") + || hasInteractionName(item, "conf_area_hide"); + } + + public static boolean isControllerActive(HabboItem item) { + return isControllerItem(item) && getState(item) == 1; + } + + public static int getState(HabboItem item) { + return Math.min(1, readIntValue(item, "state", 0)); + } + + public static int getRootX(HabboItem item) { + return readIntValue(item, "rootX", 0); + } + + public static int getRootY(HabboItem item) { + return readIntValue(item, "rootY", 0); + } + + public static int getWidth(HabboItem item) { + return readIntValue(item, "width", 0); + } + + public static int getLength(HabboItem item) { + return readIntValue(item, "length", 0); + } + + public static boolean isInvisibilityEnabled(HabboItem item) { + return readIntValue(item, "invisibility", 0) == 1; + } + + public static boolean includesWallItems(HabboItem item) { + return readIntValue(item, "wallItems", 0) == 1; + } + + public static boolean isInverted(HabboItem item) { + return readIntValue(item, "invert", 0) == 1; + } + + public static void sendState(Room room, HabboItem item) { + if (room == null || item == null || !isControllerItem(item)) { + return; + } + + room.sendComposer(new AreaHideComposer(item).compose()); + } + + public static void sendState(Room room, GameClient client) { + if (room == null || client == null) { + return; + } + + for (HabboItem item : room.getFloorItems()) { + if (!isControllerActive(item)) { + continue; + } + + client.sendResponse(new AreaHideComposer(item).compose()); + } + } + + private static int readIntValue(HabboItem item, String key, int fallback) { + if (!(item instanceof InteractionAreaHideControl) || key == null) { + return fallback; + } + + InteractionAreaHideControl areaHide = (InteractionAreaHideControl) item; + String value = areaHide.values.get(key); + + try { + return Math.max(0, Integer.parseInt(value)); + } catch (Exception ignored) { + return fallback; + } + } + + private static boolean hasInteractionName(HabboItem item, String interactionName) { + return item != null + && item.getBaseItem() != null + && item.getBaseItem().getType() == FurnitureType.FLOOR + && item.getBaseItem().getInteractionType() != null + && item.getBaseItem().getInteractionType().getName() != null + && item.getBaseItem().getInteractionType().getName().equalsIgnoreCase(interactionName); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomConfInvisSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomConfInvisSupport.java new file mode 100644 index 00000000..53b5be41 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomConfInvisSupport.java @@ -0,0 +1,107 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.interactions.InteractionConfInvisControl; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.outgoing.rooms.items.ConfInvisStateComposer; +import gnu.trove.list.array.TIntArrayList; + +import java.util.regex.Pattern; + +public final class RoomConfInvisSupport { + private RoomConfInvisSupport() { + } + + public static boolean isControllerItem(HabboItem item) { + return item instanceof InteractionConfInvisControl + || hasInteractionName(item, "wf_conf_invis_control"); + } + + public static boolean isControllerActive(HabboItem item) { + return isControllerItem(item) && "1".equals(item.getExtradata()); + } + + public static boolean isTarget(HabboItem item) { + return item != null + && item.getBaseItem() != null + && item.getBaseItem().getType() == FurnitureType.FLOOR + && hasCustomParamToken(item.getBaseItem().getCustomParams(), "is_invisible"); + } + + public static TIntArrayList collectHiddenFloorItemIds(Room room) { + TIntArrayList hiddenItemIds = new TIntArrayList(); + + if (room == null) { + return hiddenItemIds; + } + + if (!hasActiveController(room)) { + return hiddenItemIds; + } + + for (HabboItem item : room.getFloorItems()) { + if (isTarget(item)) { + hiddenItemIds.add(item.getId()); + } + } + + return hiddenItemIds; + } + + public static boolean hasActiveController(Room room) { + if (room == null) { + return false; + } + + for (HabboItem item : room.getFloorItems()) { + if (isControllerActive(item)) { + return true; + } + } + + return false; + } + + public static void sendState(Room room) { + if (room == null) { + return; + } + + room.sendComposer(new ConfInvisStateComposer(room).compose()); + } + + public static void sendState(Room room, GameClient client) { + if (room == null || client == null) { + return; + } + + client.sendResponse(new ConfInvisStateComposer(room).compose()); + } + + private static boolean hasCustomParamToken(String value, String token) { + if (value == null || token == null) { + return false; + } + + String normalized = value.trim().toLowerCase(); + + if (normalized.isEmpty()) { + return false; + } + + Pattern pattern = Pattern.compile("(^|[^a-z0-9_])" + Pattern.quote(token.toLowerCase()) + "($|[^a-z0-9_])"); + + return pattern.matcher(normalized).find(); + } + + private static boolean hasInteractionName(HabboItem item, String interactionName) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null || interactionName == null) { + return false; + } + + String currentInteractionName = item.getBaseItem().getInteractionType().getName(); + + return currentInteractionName != null && currentInteractionName.equalsIgnoreCase(interactionName); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java index 8f5bbc51..c3898db1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java @@ -377,7 +377,8 @@ public class RoomCycleManager { * Processes roller cycle. */ private void processRollers(THashSet updatedUnit) { - int rollerSpeed = this.room.getRollerSpeed(); + Integer controlledRollerSpeed = RoomQueueSpeedControlSupport.getEffectiveRollerSpeed(this.room); + int rollerSpeed = (controlledRollerSpeed != null) ? controlledRollerSpeed : this.room.getRollerSpeed(); if (rollerSpeed != -1 && this.rollerCycle >= rollerSpeed) { this.rollerCycle = 0; this.room.getRollerManager().processRollerCycle(updatedUnit, this.cycleTimestamp); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomHanditemBlockSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomHanditemBlockSupport.java new file mode 100644 index 00000000..b35025e5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomHanditemBlockSupport.java @@ -0,0 +1,65 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.items.interactions.InteractionHanditemBlockControl; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.messages.outgoing.rooms.items.HanditemBlockStateComposer; + +public final class RoomHanditemBlockSupport { + private static final String CONTROLLER_INTERACTION = "wf_conf_handitem_block"; + + private RoomHanditemBlockSupport() { + } + + public static boolean isHanditemBlocked(Room room) { + if (room == null) { + return false; + } + + for (HabboItem item : room.getFloorItems()) { + if (isActiveController(item)) { + return true; + } + } + + return false; + } + + public static boolean isActiveController(HabboItem item) { + return isControllerItem(item) && "1".equals(item.getExtradata()); + } + + public static boolean isControllerItem(HabboItem item) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + if (item instanceof InteractionHanditemBlockControl) { + return true; + } + + if (item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionName = item.getBaseItem().getInteractionType().getName(); + + return interactionName != null && interactionName.equalsIgnoreCase(CONTROLLER_INTERACTION); + } + + public static void sendState(Room room) { + if (room == null) { + return; + } + + room.sendComposer(new HanditemBlockStateComposer(room).compose()); + } + + public static void sendState(Room room, GameClient client) { + if (room == null || client == null) { + return; + } + + client.sendResponse(new HanditemBlockStateComposer(room).compose()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index a95a5cd7..9b36d828 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -1009,6 +1009,18 @@ public class RoomItemManager { if (item.getBaseItem().getType() == FurnitureType.FLOOR) { this.room.sendComposer(new RemoveFloorItemComposer(item).compose()); + if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) { + RoomConfInvisSupport.sendState(this.room); + } + + if (RoomAreaHideSupport.isControllerItem(item)) { + RoomAreaHideSupport.sendState(this.room, item); + } + + if (RoomHanditemBlockSupport.isControllerItem(item)) { + RoomHanditemBlockSupport.sendState(this.room); + } + THashSet updatedTiles = this.room.getLayout().getTilesAt( this.room.getLayout().getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), @@ -1296,13 +1308,75 @@ public class RoomItemManager { /** * Checks if furniture fits at a location with unit check option. */ + private boolean isStackPlacementBypassItem(HabboItem item) { + return item instanceof InteractionStackHelper + || item instanceof InteractionTileWalkMagic + || item instanceof InteractionStackWalkHelper; + } + + private boolean shouldPinStackHelperToFloor(HabboItem item) { + return item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic; + } + + private boolean isStackHeightHelper(HabboItem item) { + return item instanceof InteractionStackHelper + || item instanceof InteractionTileWalkMagic + || item instanceof InteractionStackWalkHelper; + } + + private HabboItem findStackHeightHelperAt(RoomTile tile, HabboItem exclude) { + if (tile == null) { + return null; + } + + for (HabboItem helper : this.getItemsAt(tile)) { + if (helper != exclude && this.isStackHeightHelper(helper)) { + return helper; + } + } + + return null; + } + + private double getMinimumTileHeight(THashSet occupiedTiles) { + double minimumHeight = 0.0D; + + for (RoomTile occupiedTile : occupiedTiles) { + minimumHeight = Math.max(minimumHeight, this.room.getLayout().getHeightAtSquare(occupiedTile.x, occupiedTile.y)); + } + + return minimumHeight; + } + + private double getConfiguredStackWalkHelperHeight(HabboItem item, THashSet occupiedTiles) { + double height = 0.0D; + + try { + if (item.getExtradata() != null && !item.getExtradata().isEmpty()) { + height = Double.parseDouble(item.getExtradata()) / 100.0D; + } + } catch (NumberFormatException ignored) { + } + + return Math.max(height, this.getMinimumTileHeight(occupiedTiles)); + } + + private double resolveStackWalkHelperHeight(HabboItem item, RoomTile tile, THashSet occupiedTiles) { + HabboItem helper = this.findStackHeightHelperAt(tile, item); + if (helper != null) { + return Math.max(helper.getZ(), this.getMinimumTileHeight(occupiedTiles)); + } + + return this.getConfiguredStackWalkHelperHeight(item, occupiedTiles); + } + public FurnitureMovementError furnitureFitsAt(RoomTile tile, HabboItem item, int rotation, boolean checkForUnits) { RoomLayout layout = this.room.getLayout(); if (!layout.fitsOnMap(tile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation)) { return FurnitureMovementError.INVALID_MOVE; } - if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic) { + if (this.isStackPlacementBypassItem(item)) { return FurnitureMovementError.NONE; } @@ -1354,7 +1428,7 @@ public class RoomItemManager { return FurnitureMovementError.INVALID_MOVE; } - if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic) { + if (this.isStackPlacementBypassItem(item)) { return FurnitureMovementError.NONE; } @@ -1463,6 +1537,18 @@ public class RoomItemManager { this.room.sendComposer( new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); + if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) { + RoomConfInvisSupport.sendState(this.room); + } + + if (RoomAreaHideSupport.isControllerItem(item)) { + RoomAreaHideSupport.sendState(this.room, item); + } + + if (RoomHanditemBlockSupport.isControllerItem(item)) { + RoomHanditemBlockSupport.sendState(this.room); + } + for (RoomTile t : occupiedTiles) { this.room.updateHabbosAt(t.x, t.y); this.room.updateBotsAt(t.x, t.y); @@ -1530,9 +1616,7 @@ public class RoomItemManager { rotation %= 8; - boolean magicTile = - item instanceof InteractionStackHelper || - item instanceof InteractionTileWalkMagic; + boolean magicTile = this.isStackPlacementBypassItem(item); THashSet occupiedTiles = layout.getTilesAt( tile, @@ -1595,9 +1679,11 @@ public class RoomItemManager { item.setY(tile.y); item.setZ(z); - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + (item.getZ() * 100)); + } else if (item instanceof InteractionStackWalkHelper) { + item.setZ(this.resolveStackWalkHelperHeight(item, tile, occupiedTiles)); } if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { @@ -1689,9 +1775,7 @@ public class RoomItemManager { rotation %= 8; - boolean magicTile = - item instanceof InteractionStackHelper || - item instanceof InteractionTileWalkMagic; + boolean magicTile = this.isStackPlacementBypassItem(item); THashSet occupiedTiles = layout.getTilesAt( tile, @@ -1751,9 +1835,11 @@ public class RoomItemManager { item.setY(tile.y); item.setZ(z); - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + (item.getZ() * 100)); + } else if (item instanceof InteractionStackWalkHelper) { + item.setZ(this.resolveStackWalkHelperHeight(item, tile, occupiedTiles)); } if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { @@ -1845,10 +1931,9 @@ public class RoomItemManager { pluginHelper = event.hasPluginHelper(); } - boolean magicTile = item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic; + boolean magicTile = this.isStackPlacementBypassItem(item); - java.util.Optional stackHelper = this.getItemsAt(tile).stream() - .filter(i -> i instanceof InteractionStackHelper).findAny(); + HabboItem stackHelper = this.findStackHeightHelperAt(tile, item); // Check if can be placed at new position THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), @@ -1858,7 +1943,7 @@ public class RoomItemManager { HabboItem topItem = this.getTopItemAt(occupiedTiles, null); - if (!stackHelper.isPresent() && !pluginHelper) { + if (stackHelper == null && !pluginHelper) { if (oldLocation != tile) { for (RoomTile t : occupiedTiles) { HabboItem tileTopItem = this.getTopItemAt(t.x, t.y); @@ -1915,7 +2000,7 @@ public class RoomItemManager { } } - if ((!stackHelper.isPresent() && topItem != null && topItem != item && !topItem.getBaseItem() + if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem() .allowStack()) || (topItem != null && topItem != item && topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item) > Room.MAXIMUM_FURNI_HEIGHT)) { @@ -1927,9 +2012,10 @@ public class RoomItemManager { // Place at new position double height; - if (stackHelper.isPresent()) { - height = stackHelper.get().getExtradata().isEmpty() ? Double.parseDouble("0.0") - : (Double.parseDouble(stackHelper.get().getExtradata()) / 100); + if (stackHelper != null) { + height = stackHelper.getZ(); + } else if (item instanceof InteractionStackWalkHelper) { + height = this.resolveStackWalkHelperHeight(item, tile, occupiedTiles); } else if (item == topItem) { height = item.getZ(); } else if (magicTile) { @@ -1980,7 +2066,7 @@ public class RoomItemManager { item.setX(tile.x); item.setY(tile.y); item.setZ(height); - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + item.getZ() * 100); } @@ -2054,10 +2140,9 @@ public class RoomItemManager { pluginHelper = event.hasPluginHelper(); } - boolean magicTile = item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic; + boolean magicTile = this.isStackPlacementBypassItem(item); - java.util.Optional stackHelper = this.getItemsAt(tile).stream() - .filter(i -> i instanceof InteractionStackHelper).findAny(); + HabboItem stackHelper = this.findStackHeightHelperAt(tile, item); THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation); @@ -2066,7 +2151,7 @@ public class RoomItemManager { HabboItem topItem = this.getTopPhysicsItemAt(occupiedTiles, null, physics); - if (!stackHelper.isPresent() && !pluginHelper) { + if (stackHelper == null && !pluginHelper) { if (oldLocation != tile) { for (RoomTile t : occupiedTiles) { HabboItem tileTopItem = this.getTopPhysicsItemAt(t.x, t.y, item, physics); @@ -2118,7 +2203,7 @@ public class RoomItemManager { } } - if ((!stackHelper.isPresent() && topItem != null && topItem != item && !topItem.getBaseItem() + if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem() .allowStack()) || (topItem != null && topItem != item && topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item) > Room.MAXIMUM_FURNI_HEIGHT)) { @@ -2129,9 +2214,10 @@ public class RoomItemManager { double height; - if (stackHelper.isPresent()) { - height = stackHelper.get().getExtradata().isEmpty() ? Double.parseDouble("0.0") - : (Double.parseDouble(stackHelper.get().getExtradata()) / 100); + if (stackHelper != null) { + height = stackHelper.getZ(); + } else if (item instanceof InteractionStackWalkHelper) { + height = this.resolveStackWalkHelperHeight(item, tile, occupiedTiles); } else if (item == topItem) { height = item.getZ(); } else if (magicTile) { @@ -2182,7 +2268,7 @@ public class RoomItemManager { item.setX(tile.x); item.setY(tile.y); item.setZ(height); - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + item.getZ() * 100); } @@ -2237,7 +2323,7 @@ public class RoomItemManager { * Slides furniture to a new position. */ public FurnitureMovementError slideFurniTo(HabboItem item, RoomTile tile, int rotation) { - boolean magicTile = item instanceof InteractionStackHelper; + boolean magicTile = this.isStackPlacementBypassItem(item); RoomLayout layout = this.room.getLayout(); @@ -2257,9 +2343,11 @@ public class RoomItemManager { item.setRotation(rotation); // Place at new position - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + item.getZ() * 100); + } else if (item instanceof InteractionStackWalkHelper) { + item.setZ(this.resolveStackWalkHelperHeight(item, tile, occupiedTiles)); } if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { item.setZ(Room.MAXIMUM_FURNI_HEIGHT); @@ -2410,12 +2498,17 @@ public class RoomItemManager { return height; } + double helperHeight = Double.NEGATIVE_INFINITY; for (HabboItem item : this.getPhysicsItemsAt(tile, exclude, physics)) { - if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic) { - return item.getZ(); + if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { + helperHeight = Math.max(helperHeight, item.getZ()); } } + if (helperHeight != Double.NEGATIVE_INFINITY) { + return helperHeight; + } + HabboItem topItem = this.getTopPhysicsItemAt(x, y, exclude, physics); if (topItem != null) { return topItem.getZ() + (topItem.getBaseItem().allowSit() ? 0 : Item.getCurrentHeight(topItem)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java index 08d9ccca..fd25c1e7 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java @@ -34,6 +34,8 @@ import com.eu.habbo.messages.outgoing.polls.PollStartComposer; import com.eu.habbo.messages.outgoing.polls.infobus.SimplePollAnswersComposer; import com.eu.habbo.messages.outgoing.polls.infobus.SimplePollStartComposer; import com.eu.habbo.messages.outgoing.rooms.*; +import com.eu.habbo.messages.outgoing.rooms.items.ConfInvisStateComposer; +import com.eu.habbo.messages.outgoing.rooms.items.HanditemBlockStateComposer; import com.eu.habbo.messages.outgoing.rooms.items.RoomFloorItemsComposer; import com.eu.habbo.messages.outgoing.rooms.items.RoomWallItemsComposer; import com.eu.habbo.messages.outgoing.rooms.pets.RoomPetComposer; @@ -886,6 +888,10 @@ public class RoomManager { floorItems.clear(); } + habbo.getClient().sendResponse(new ConfInvisStateComposer(room).compose()); + RoomAreaHideSupport.sendState(room, habbo.getClient()); + habbo.getClient().sendResponse(new HanditemBlockStateComposer(room).compose()); + if (!room.getCurrentPets().isEmpty()) { habbo.getClient().sendResponse(new RoomPetComposer(room.getCurrentPets())); for (Pet pet : room.getCurrentPets().valueCollection()) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomQueueSpeedControlSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomQueueSpeedControlSupport.java new file mode 100644 index 00000000..18019c9f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomQueueSpeedControlSupport.java @@ -0,0 +1,77 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.items.interactions.InteractionQueueSpeedControl; +import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; +import com.eu.habbo.habbohotel.users.HabboItem; + +public final class RoomQueueSpeedControlSupport { + private static final String CONTROLLER_INTERACTION = "wf_conf_queue_speed"; + + private RoomQueueSpeedControlSupport() { + } + + public static Integer getEffectiveRollerSpeed(Room room) { + HabboItem controller = getControllerItem(room); + return controller != null ? InteractionQueueSpeedControl.toRollerSpeed(controller.getExtradata()) : null; + } + + public static int getEffectiveRollerIntervalMs(Room room) { + Integer effectiveRollerSpeed = getEffectiveRollerSpeed(room); + + if (effectiveRollerSpeed != null) { + return toRollerIntervalMs(effectiveRollerSpeed); + } + + if (room == null) { + return InteractionRoller.DELAY; + } + + return toRollerIntervalMs(room.getRollerSpeed()); + } + + private static int toRollerIntervalMs(int rollerSpeed) { + if (rollerSpeed < 0) { + return InteractionRoller.DELAY; + } + + return (rollerSpeed + 1) * 500; + } + + private static boolean isControllerItem(HabboItem item) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + if (item instanceof InteractionQueueSpeedControl) { + return true; + } + + if (item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionName = item.getBaseItem().getInteractionType().getName(); + + return interactionName != null && interactionName.equalsIgnoreCase(CONTROLLER_INTERACTION); + } + + private static HabboItem getControllerItem(Room room) { + if (room == null) { + return null; + } + + for (HabboItem item : room.getFloorItems()) { + if (!isControllerItem(item)) { + continue; + } + + if (item instanceof InteractionQueueSpeedControl) { + ((InteractionQueueSpeedControl) item).ensureAnimationLoop(room); + } + + return item; + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java index 9d564ece..318e6f78 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java @@ -354,7 +354,7 @@ public class RoomRollerManager { LOGGER.error("Caught exception", e); } } - }, this.room.getRollerSpeed() == 0 ? 250 : InteractionRoller.DELAY); + }, RoomQueueSpeedControlSupport.getEffectiveRollerIntervalMs(this.room)); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java index c87be418..e585b675 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionStackHelper; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.plugin.events.furniture.FurnitureStackHeightEvent; @@ -149,7 +150,7 @@ public class RoomTileManager { result = overriddenState; } - if (this.room.getItemManager().getItemsAt(tile).stream().anyMatch(i -> i instanceof InteractionTileWalkMagic)) { + if (this.room.getItemManager().getItemsAt(tile).stream().anyMatch(i -> i instanceof InteractionTileWalkMagic || i instanceof InteractionStackWalkHelper)) { result = RoomTileState.OPEN; } @@ -211,14 +212,20 @@ public class RoomTileManager { boolean canStack = true; THashSet stackHelpers = this.room.getItemManager().getItemsAt(InteractionStackHelper.class, x, y); + stackHelpers.addAll(this.room.getItemManager().getItemsAt(InteractionStackWalkHelper.class, x, y)); stackHelpers.addAll(this.room.getItemManager().getItemsAt(InteractionTileWalkMagic.class, x, y)); if (stackHelpers.size() > 0) { + double helperHeight = Double.NEGATIVE_INFINITY; for (HabboItem item : stackHelpers) { if (item == exclude) { continue; } - return calculateHeightmap ? item.getZ() * 256.0D : item.getZ(); + helperHeight = Math.max(helperHeight, item.getZ()); + } + + if (helperHeight != Double.NEGATIVE_INFINITY) { + return calculateHeightmap ? helperHeight * 256.0D : helperHeight; } } @@ -425,6 +432,10 @@ public class RoomTileManager { HabboItem topItem = null; boolean canWalk = true; THashSet items = this.room.getItemManager().getItemsAt(roomTile); + if (items != null && items.stream().anyMatch(item -> item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper)) { + return true; + } + if (items != null) { for (HabboItem item : items) { if (topItem == null) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java index bda55501..983ef85b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.items.interactions.InteractionWater; import com.eu.habbo.habbohotel.items.interactions.InteractionWaterItem; import com.eu.habbo.habbohotel.items.interactions.interfaces.ConditionalGate; @@ -325,7 +326,7 @@ public class RoomUnit { } Optional stackHelper = room.getItemsAt(next).stream() - .filter(i -> i instanceof InteractionTileWalkMagic).findAny(); + .filter(i -> i instanceof InteractionTileWalkMagic || i instanceof InteractionStackWalkHelper).findAny(); if (stackHelper.isPresent()) { zHeight = stackHelper.get().getZ(); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomWiredDisableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomWiredDisableSupport.java new file mode 100644 index 00000000..9aa33f59 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomWiredDisableSupport.java @@ -0,0 +1,47 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredDisableControl; +import com.eu.habbo.habbohotel.users.HabboItem; + +public final class RoomWiredDisableSupport { + private static final String CONTROLLER_INTERACTION = "wf_conf_wired_disable"; + + private RoomWiredDisableSupport() { + } + + public static boolean isWiredDisabled(Room room) { + if (room == null) { + return false; + } + + for (HabboItem item : room.getFloorItems()) { + if (isActiveController(item)) { + return true; + } + } + + return false; + } + + public static boolean isActiveController(HabboItem item) { + return isControllerItem(item) && "1".equals(item.getExtradata()); + } + + public static boolean isControllerItem(HabboItem item) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + if (item instanceof InteractionWiredDisableControl) { + return true; + } + + if (item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionName = item.getBaseItem().getInteractionType().getName(); + + return interactionName != null && interactionName.equalsIgnoreCase(CONTROLLER_INTERACTION); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java index 5e81da8d..a0451992 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java @@ -16,6 +16,7 @@ import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomWiredDisableSupport; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; @@ -71,6 +72,9 @@ public class WiredHandler { if (room == null) return false; + if (RoomWiredDisableSupport.isWiredDisabled(room)) + return false; + if (!room.isLoaded()) return false; @@ -118,6 +122,9 @@ public class WiredHandler { if (room == null) return false; + if (RoomWiredDisableSupport.isWiredDisabled(room)) + return false; + if (!room.isLoaded()) return false; @@ -160,6 +167,9 @@ public class WiredHandler { long millis = System.currentTimeMillis(); LegacyExecutionPlan executionPlan = new LegacyExecutionPlan(); + if (RoomWiredDisableSupport.isWiredDisabled(room)) + return false; + if(handle(trigger, roomUnit, room, stuff, executionPlan)) { triggerEffects(executionPlan.effects, roomUnit, room, stuff, millis, executionPlan.executeInOrder); return true; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java index 82456ac9..9d264065 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java @@ -10,6 +10,7 @@ import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectTrigg import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboClicksUser; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomWiredDisableSupport; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; @@ -231,6 +232,10 @@ public final class WiredManager { if (!isEnabled() || engine == null) { return false; } + + if (event == null || RoomWiredDisableSupport.isWiredDisabled(event.getRoom())) { + return false; + } return engine.handleEvent(event); } @@ -348,6 +353,10 @@ public final class WiredManager { return false; } + if (RoomWiredDisableSupport.isWiredDisabled(room)) { + return false; + } + WiredEvent event = WiredEvents.userSays(room, user, message, chatType, chatStyle); return engine.shouldSuppressUserSaysOutput(event); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java index 0f293184..88d62d20 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java @@ -77,6 +77,7 @@ public class RoomPlaceItemEvent extends MessageHandler { if ((rentSpace != null || buildArea != null) && !room.hasRights(this.client.getHabbo())) { if (item instanceof InteractionRoller || item instanceof InteractionStackHelper || + item instanceof InteractionStackWalkHelper || item instanceof InteractionWired || item instanceof InteractionBackgroundToner || item instanceof InteractionRoomAds || diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java index edc29cc2..c3b1d337 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.rooms.items; import com.eu.habbo.habbohotel.items.interactions.InteractionStackHelper; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomTile; @@ -21,7 +22,7 @@ public class SetStackHelperHeightEvent extends MessageHandler { if (this.client.getHabbo().getHabboInfo().getId() == this.client.getHabbo().getHabboInfo().getCurrentRoom().getOwnerId() || this.client.getHabbo().getHabboInfo().getCurrentRoom().hasRights(this.client.getHabbo())) { HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId); - if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic) { + if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); RoomTile itemTile = room.getLayout().getTile(item.getX(), item.getY()); double stackerHeight = this.packet.readInt(); @@ -51,7 +52,7 @@ public class SetStackHelperHeightEvent extends MessageHandler { item.setExtradata((int) (height * 100) + ""); item.needsUpdate(true); - if (item instanceof InteractionTileWalkMagic) { + if (item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { for (RoomTile t : tiles) { this.client.getHabbo().getHabboInfo().getCurrentRoom().updateHabbosAt(t.x, t.y); this.client.getHabbo().getHabboInfo().getCurrentRoom().updateBotsAt(t.x, t.y); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java index 9342e045..dd6333f7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.rooms.users; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomHanditemBlockSupport; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.threading.runnables.HabboGiveHandItemToHabbo; @@ -18,6 +19,10 @@ public class RoomUserGiveHandItemEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { + if (RoomHanditemBlockSupport.isHanditemBlocked(room)) { + return; + } + Habbo target = room.getHabbo(userId); if (target != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 3abf3fbc..36ef55a3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -123,6 +123,8 @@ public class Outgoing { public final static int WiredMonitorDataComposer = 5101; // CUSTOM public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM + public final static int ConfInvisStateComposer = 5104; // CUSTOM + public final static int AreaHideComposer = 6001; // CUSTOM public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768 public final static int MarketplaceConfigComposer = 1823; // PRODUCTION-201611291003-338511768 public final static int AddBotComposer = 1352; // PRODUCTION-201611291003-338511768 @@ -328,6 +330,7 @@ public class Outgoing { public final static int VerifyMobilePhoneCodeWindowComposer = 800; // PRODUCTION-201611291003-338511768 public final static int VerifyMobilePhoneDoneComposer = 91; // PRODUCTION-201611291003-338511768 public final static int RoomUserReceivedHandItemComposer = 354; // PRODUCTION-201611291003-338511768 + public final static int HanditemBlockStateComposer = 5105; public final static int MutedWhisperComposer = 826; // PRODUCTION-201611291003-338511768 public final static int UnknownHintComposer = 1787; // PRODUCTION-201611291003-338511768 public final static int BullyReportClosedComposer = 2674; // PRODUCTION-201611291003-338511768 diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java index dde6076a..94c29a61 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java @@ -19,7 +19,13 @@ public class AddFloorItemComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.AddFloorItemComposer); this.item.serializeFloorData(this.response); - this.response.appendInt(this.item instanceof InteractionGift ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) : (this.item instanceof InteractionMusicDisc ? ((InteractionMusicDisc) this.item).getSongId() : 1)); + this.response.appendInt( + this.item instanceof InteractionGift + ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) + : (this.item instanceof InteractionMusicDisc + ? ((InteractionMusicDisc) this.item).getSongId() + : (this.item instanceof InteractionStackWalkHelper ? 2147483001 : 1)) + ); this.item.serializeExtradata(this.response); this.response.appendInt(-1); this.response.appendInt(this.item instanceof InteractionTeleport || this.item instanceof InteractionSwitch || this.item instanceof InteractionSwitchRemoteControl || this.item instanceof InteractionVendingMachine || this.item instanceof InteractionInformationTerminal || this.item instanceof InteractionPostIt|| this.item instanceof InteractionPuzzleBox ? 2 : this.item.isUsable() ? 1 : 0); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AreaHideComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AreaHideComposer.java new file mode 100644 index 00000000..c7e48e55 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AreaHideComposer.java @@ -0,0 +1,30 @@ +package com.eu.habbo.messages.outgoing.rooms.items; + +import com.eu.habbo.habbohotel.rooms.RoomAreaHideSupport; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class AreaHideComposer extends MessageComposer { + private final HabboItem item; + + public AreaHideComposer(HabboItem item) { + this.item = item; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.AreaHideComposer); + this.response.appendInt(this.item.getId()); + this.response.appendBoolean(RoomAreaHideSupport.isControllerActive(this.item)); + this.response.appendInt(RoomAreaHideSupport.getRootX(this.item)); + this.response.appendInt(RoomAreaHideSupport.getRootY(this.item)); + this.response.appendInt(RoomAreaHideSupport.getWidth(this.item)); + this.response.appendInt(RoomAreaHideSupport.getLength(this.item)); + this.response.appendBoolean(RoomAreaHideSupport.isInverted(this.item)); + this.response.appendBoolean(RoomAreaHideSupport.includesWallItems(this.item)); + this.response.appendBoolean(RoomAreaHideSupport.isInvisibilityEnabled(this.item)); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/ConfInvisStateComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/ConfInvisStateComposer.java new file mode 100644 index 00000000..21ef034b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/ConfInvisStateComposer.java @@ -0,0 +1,34 @@ +package com.eu.habbo.messages.outgoing.rooms.items; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomConfInvisSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; +import gnu.trove.list.array.TIntArrayList; + +public class ConfInvisStateComposer extends MessageComposer { + private final int roomId; + private final boolean active; + private final TIntArrayList hiddenItemIds; + + public ConfInvisStateComposer(Room room) { + this.roomId = (room != null) ? room.getId() : 0; + this.active = RoomConfInvisSupport.hasActiveController(room); + this.hiddenItemIds = RoomConfInvisSupport.collectHiddenFloorItemIds(room); + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.ConfInvisStateComposer); + this.response.appendInt(this.roomId); + this.response.appendBoolean(this.active); + this.response.appendInt(this.hiddenItemIds.size()); + + for (int i = 0; i < this.hiddenItemIds.size(); i++) { + this.response.appendInt(this.hiddenItemIds.get(i)); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java index 77b292ea..88b4dd64 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.outgoing.rooms.items; import com.eu.habbo.habbohotel.items.interactions.InteractionGift; import com.eu.habbo.habbohotel.items.interactions.InteractionMusicDisc; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; @@ -18,7 +19,13 @@ public class FloorItemUpdateComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.FloorItemUpdateComposer); this.item.serializeFloorData(this.response); - this.response.appendInt(this.item instanceof InteractionGift ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) : (this.item instanceof InteractionMusicDisc ? ((InteractionMusicDisc) this.item).getSongId() : item.isUsable() ? 0 : 0)); + this.response.appendInt( + this.item instanceof InteractionGift + ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) + : (this.item instanceof InteractionMusicDisc + ? ((InteractionMusicDisc) this.item).getSongId() + : (this.item instanceof InteractionStackWalkHelper ? 2147483001 : 0)) + ); this.item.serializeExtradata(this.response); this.response.appendInt(-1); this.response.appendInt(0); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/HanditemBlockStateComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/HanditemBlockStateComposer.java new file mode 100644 index 00000000..80fa7dce --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/HanditemBlockStateComposer.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.outgoing.rooms.items; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomHanditemBlockSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class HanditemBlockStateComposer extends MessageComposer { + private final int roomId; + private final boolean blocked; + + public HanditemBlockStateComposer(Room room) { + this.roomId = (room != null) ? room.getId() : 0; + this.blocked = RoomHanditemBlockSupport.isHanditemBlocked(room); + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.HanditemBlockStateComposer); + this.response.appendInt(this.roomId); + this.response.appendBoolean(this.blocked); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java index 162935ab..2045879c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java @@ -41,7 +41,13 @@ public class RoomFloorItemsComposer extends MessageComposer { for (HabboItem item : this.items) { item.serializeFloorData(this.response); - this.response.appendInt(item instanceof InteractionGift ? ((((InteractionGift) item).getColorId() * 1000) + ((InteractionGift) item).getRibbonId()) : (item instanceof InteractionMusicDisc ? ((InteractionMusicDisc) item).getSongId() : 1)); + this.response.appendInt( + item instanceof InteractionGift + ? ((((InteractionGift) item).getColorId() * 1000) + ((InteractionGift) item).getRibbonId()) + : (item instanceof InteractionMusicDisc + ? ((InteractionMusicDisc) item).getSongId() + : (item instanceof InteractionStackWalkHelper ? 2147483001 : 1)) + ); item.serializeExtradata(this.response); this.response.appendInt(-1); this.response.appendInt(item instanceof InteractionTeleport || item instanceof InteractionSwitch || item instanceof InteractionSwitchRemoteControl || item instanceof InteractionVendingMachine || item instanceof InteractionInformationTerminal || item instanceof InteractionPostIt || item instanceof InteractionPuzzleBox ? 2 : item.isUsable() ? 1 : 0); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java index 0628a478..4a356e7a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; import com.eu.habbo.habbohotel.pets.Pet; import com.eu.habbo.habbohotel.pets.RideablePet; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomQueueSpeedControlSupport; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnitType; @@ -157,7 +158,7 @@ public class RoomUnitOnRollerComposer extends MessageComposer { } } } - }, this.room.getRollerSpeed() == 0 ? 250 : InteractionRoller.DELAY); + }, RoomQueueSpeedControlSupport.getEffectiveRollerIntervalMs(this.room)); /* RoomTile rollerTile = room.getLayout().getTile(this.roller.getX(), this.roller.getY()); Emulator.getThreading().run(() -> { @@ -177,7 +178,7 @@ public class RoomUnitOnRollerComposer extends MessageComposer { } } } - }, this.room.getRollerSpeed() == 0 ? 250 : InteractionRoller.DELAY); + }, RoomQueueSpeedControlSupport.getEffectiveRollerIntervalMs(this.room)); */ } else { this.roomUnit.setLocation(this.newLocation); diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java index 16a59ec5..a3cf6470 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java @@ -1,6 +1,8 @@ package com.eu.habbo.threading.runnables; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomHanditemBlockSupport; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserHandItemComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserReceivedHandItemComposer; @@ -21,6 +23,12 @@ public class HabboGiveHandItemToHabbo implements Runnable { if (this.from.getHabboInfo().getCurrentRoom() != this.target.getHabboInfo().getCurrentRoom()) return; + Room room = this.from.getHabboInfo().getCurrentRoom(); + + if (RoomHanditemBlockSupport.isHanditemBlocked(room)) { + return; + } + int itemId = this.from.getRoomUnit().getHandItem(); if (itemId > 0) { From 3efcca1e342fdfb9beb8a10ece08ac7f7e6f9c63 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Sat, 4 Apr 2026 15:57:43 +0200 Subject: [PATCH 4/8] feat: update wired movement and show message behavior --- .../008_add_show_message_chat_bubbles.sql | 25 ++ .../wired/effects/WiredEffectAlert.java | 21 +- .../WiredEffectChangeVariableValue.java | 18 +- .../effects/WiredEffectMoveRotateUser.java | 114 +------- .../wired/effects/WiredEffectSendSignal.java | 10 +- .../wired/effects/WiredEffectWhisper.java | 141 ++++++++- .../wired/extra/WiredExtraVariableEcho.java | 12 +- .../rooms/RoomChatMessageBubbles.java | 20 +- .../habbohotel/wired/core/WiredEngine.java | 111 ++----- .../core/WiredInternalVariableSupport.java | 245 +++++++++++++++- .../habbohotel/wired/core/WiredManager.java | 64 +++- .../wired/core/WiredMoveCarryHelper.java | 19 +- .../core/WiredSelectionFilterSupport.java | 204 +++++++++++++ .../wired/core/WiredSourceUtil.java | 133 +++------ .../wired/core/WiredTriggerSourceUtil.java | 78 +---- .../wired/core/WiredUserMovementHelper.java | 275 +++++++++++++++--- .../rooms/users/RoomUserWalkEvent.java | 4 + 17 files changed, 1057 insertions(+), 437 deletions(-) create mode 100644 Database Updates/008_add_show_message_chat_bubbles.sql create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSelectionFilterSupport.java diff --git a/Database Updates/008_add_show_message_chat_bubbles.sql b/Database Updates/008_add_show_message_chat_bubbles.sql new file mode 100644 index 00000000..abf71bef --- /dev/null +++ b/Database Updates/008_add_show_message_chat_bubbles.sql @@ -0,0 +1,25 @@ +INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES +(200, 'SHOW_MESSAGE_RED', '', 1, 0), +(201, 'SHOW_MESSAGE_GREEN', '', 1, 0), +(202, 'SHOW_MESSAGE_BLUE', '', 1, 0), +(210, 'SHOW_MESSAGE_ALERT', '', 1, 0), +(211, 'SHOW_MESSAGE_INFO', '', 1, 0), +(212, 'SHOW_MESSAGE_WARNING', '', 1, 0), +(220, 'SHOW_MESSAGE_WRONG', '', 1, 0), +(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0), +(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0), +(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0), +(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0), +(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0), +(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0), +(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0), +(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0), +(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0), +(250, 'SHOW_MESSAGE_SKULL', '', 1, 0), +(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0), +(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `permission` = VALUES(`permission`), + `overridable` = VALUES(`overridable`), + `triggers_talking_furniture` = VALUES(`triggers_talking_furniture`); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java index e149b872..a6c9510e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java @@ -3,12 +3,14 @@ package com.eu.habbo.habbohotel.items.interactions.wired.effects; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; public class WiredEffectAlert extends WiredEffectWhisper { public WiredEffectAlert(ResultSet set, Item baseItem) throws SQLException { @@ -22,14 +24,25 @@ public class WiredEffectAlert extends WiredEffectWhisper { @Override public void execute(WiredContext ctx) { Room room = ctx.room(); + List sourceUsers = resolveUsers(ctx); + List recipients = resolveRecipients(ctx, sourceUsers); + Habbo sharedSourceHabbo = (this.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) + ? resolveMessageSourceHabbo(ctx, sourceUsers) + : null; - for (com.eu.habbo.habbohotel.rooms.RoomUnit unit : resolveUsers(ctx)) { - Habbo habbo = room.getHabbo(unit); - if (habbo == null) continue; + for (Habbo habbo : recipients) { + if (!shouldDeliverToRecipient(ctx, habbo)) { + continue; + } + + Habbo referenceHabbo = (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo; + String username = (referenceHabbo != null && referenceHabbo.getHabboInfo() != null) + ? referenceHabbo.getHabboInfo().getUsername() + : ""; String message = this.message .replace("%online%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "") - .replace("%username%", habbo.getHabboInfo().getUsername()) + .replace("%username%", username) .replace("%roomsloaded%", Emulator.getGameEnvironment().getRoomManager().loadedRoomsCount() + ""); habbo.alert(WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, message)); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java index 81fe314a..5e7e5c37 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java @@ -595,7 +595,13 @@ public class WiredEffectChangeVariableValue extends InteractionWiredEffect { } private boolean writeUserInternalValue(Room room, RoomUnit roomUnit, String key, int value) { - return WiredInternalVariableSupport.writeUserValue(room, roomUnit, key, value); + return WiredInternalVariableSupport.writeUserValue( + room, + roomUnit, + key, + value, + WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, + false); } private Integer readFurniInternalValue(Room room, HabboItem item, String key) { @@ -676,7 +682,15 @@ public class WiredEffectChangeVariableValue extends InteractionWiredEffect { if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); - return WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), 0, true); + return WiredUserMovementHelper.moveUser( + room, + roomUnit, + targetTile, + targetZ, + roomUnit.getBodyRotation(), + roomUnit.getHeadRotation(), + WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, + false); } private boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java index 50e9b916..5df4fdc2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java @@ -10,7 +10,6 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomTileState; import com.eu.habbo.habbohotel.rooms.RoomUnit; -import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; import com.eu.habbo.habbohotel.rooms.RoomUserRotation; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; @@ -19,7 +18,6 @@ import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics; import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; -import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import gnu.trove.procedure.TObjectProcedure; @@ -32,10 +30,6 @@ import java.util.List; public class WiredEffectMoveRotateUser extends InteractionWiredEffect { private static final int ROTATION_CLOCKWISE = 8; private static final int ROTATION_COUNTER_CLOCKWISE = 9; - private static final String CACHE_ACTIVE_UNTIL = "wired.move_rotate_user.active_until"; - private static final String CACHE_WALK_IN_PLACE_UNTIL = "wired.move_rotate_user.walk_in_place_until"; - private static final int WALK_IN_PLACE_DURATION_MS = 550; - private static final int ROTATION_ACTIVE_WINDOW_MS = 250; public static final WiredEffectType type = WiredEffectType.MOVE_ROTATE_USER; @@ -64,15 +58,21 @@ public class WiredEffectMoveRotateUser extends InteractionWiredEffect { boolean hasRotation = this.rotationDirection >= 0; RoomUserRotation targetBodyRotation = hasRotation ? this.getTargetRotation(roomUnit) : roomUnit.getBodyRotation(); RoomUserRotation targetHeadRotation = hasRotation ? targetBodyRotation : roomUnit.getHeadRotation(); + + if (roomUnit.isWalking()) { + if (hasRotation) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetBodyRotation, targetHeadRotation); + } + continue; + } + RoomTile targetTile = (this.movementDirection >= 0) ? this.getTargetTile(room, roomUnit, this.movementDirection) : null; boolean canMove = this.canMoveTo(room, roomUnit, targetTile, movementPhysics); boolean noAnimation = WiredMoveCarryHelper.hasNoAnimationExtra(room, this); int animationDuration = noAnimation ? 0 : WiredMoveCarryHelper.getAnimationDuration(room, this, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION); - int activeWindowMs = this.resolveActiveWindow(canMove, hasRotation, noAnimation, animationDuration); if (canMove) { double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); - this.markActive(roomUnit, activeWindowMs); if (!WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, targetBodyRotation, targetHeadRotation, animationDuration, noAnimation, movementPhysics)) { if (hasRotation) { @@ -83,7 +83,6 @@ public class WiredEffectMoveRotateUser extends InteractionWiredEffect { } if (hasRotation) { - this.markActive(roomUnit, activeWindowMs); WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetBodyRotation, targetHeadRotation); } } @@ -272,103 +271,8 @@ public class WiredEffectMoveRotateUser extends InteractionWiredEffect { return WiredUserMovementHelper.canMoveTo(room, roomUnit, targetTile, movementPhysics); } - private void markActive(RoomUnit roomUnit, int durationMs) { - if (roomUnit == null || durationMs <= 0) { - return; - } - - long activeUntil = System.currentTimeMillis() + durationMs; - roomUnit.getCacheable().put(CACHE_ACTIVE_UNTIL, activeUntil); - } - - private int resolveActiveWindow(boolean canMove, boolean hasRotation, boolean noAnimation, int animationDuration) { - if (noAnimation) { - return 0; - } - - if (canMove) { - return Math.max(1, animationDuration); - } - - if (hasRotation) { - return ROTATION_ACTIVE_WINDOW_MS; - } - - return 0; - } - public static boolean handleWalkWhileActive(Room room, RoomUnit roomUnit, RoomTile targetTile) { - if (room == null || roomUnit == null || !isActive(roomUnit)) { - return false; - } - - long walkInPlaceUntil = System.currentTimeMillis() + WALK_IN_PLACE_DURATION_MS; - roomUnit.getCacheable().put(CACHE_WALK_IN_PLACE_UNTIL, walkInPlaceUntil); - roomUnit.stopWalking(); - roomUnit.removeStatus(RoomUnitStatus.MOVE); - roomUnit.setStatus(RoomUnitStatus.MOVE, roomUnit.getX() + "," + roomUnit.getY() + "," + roomUnit.getZ()); - roomUnit.statusUpdate(false); - room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); - - Emulator.getThreading().run(() -> clearWalkInPlace(room, roomUnit, walkInPlaceUntil), WALK_IN_PLACE_DURATION_MS); - return true; - } - - private static boolean isActive(RoomUnit roomUnit) { - Long activeUntil = getCachedTimestamp(roomUnit, CACHE_ACTIVE_UNTIL); - return activeUntil != null && activeUntil > System.currentTimeMillis(); - } - - private static boolean isWalkInPlaceActive(RoomUnit roomUnit) { - Long walkInPlaceUntil = getCachedTimestamp(roomUnit, CACHE_WALK_IN_PLACE_UNTIL); - - if (walkInPlaceUntil == null) { - return false; - } - - if (walkInPlaceUntil <= System.currentTimeMillis()) { - roomUnit.getCacheable().remove(CACHE_WALK_IN_PLACE_UNTIL); - return false; - } - - return true; - } - - private static Long getCachedTimestamp(RoomUnit roomUnit, String key) { - if (roomUnit == null || key == null) { - return null; - } - - Object value = roomUnit.getCacheable().get(key); - - if (value instanceof Long) { - return (Long) value; - } - - if (value instanceof Number) { - return ((Number) value).longValue(); - } - - return null; - } - - private static void clearWalkInPlace(Room room, RoomUnit roomUnit, long expectedUntil) { - if (room == null || roomUnit == null || !room.isLoaded()) { - return; - } - - Long currentUntil = getCachedTimestamp(roomUnit, CACHE_WALK_IN_PLACE_UNTIL); - if (currentUntil == null || currentUntil.longValue() != expectedUntil) { - return; - } - - roomUnit.getCacheable().remove(CACHE_WALK_IN_PLACE_UNTIL); - - if (roomUnit.hasStatus(RoomUnitStatus.MOVE) && !roomUnit.isWalking()) { - roomUnit.removeStatus(RoomUnitStatus.MOVE); - roomUnit.statusUpdate(false); - room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); - } + return false; } static class JsonData { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index fd2b5a45..f57ebd92 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -118,17 +118,19 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { : Collections.singletonList(defaultFurni); int nextDepth = currentDepth + 1; + boolean isolateBranchContext = (signalPerUser && forwardedUsers.size() > 1) + || (signalPerFurni && forwardedFurni.size() > 1); for (RoomUnit user : usersToSend) { for (HabboItem sourceItem : furniToSend) { for (HabboItem antenna : resolvedAntennas) { - fireSignalAtAntenna(ctx, room, antenna, user, sourceItem, nextDepth); + fireSignalAtAntenna(ctx, room, antenna, user, sourceItem, nextDepth, isolateBranchContext); } } } } - private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth) { + private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth, boolean isolateBranchContext) { if (antenna == null) return; RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); if (tile == null) return; @@ -146,13 +148,13 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .signalChannel(signalChannel) .signalUserCount(actor != null ? 1 : 0) .signalFurniCount(sourceItem != null ? 1 : 0) - .contextVariableScope(ctx.contextVariables()) + .contextVariableScope(isolateBranchContext ? ctx.contextVariables().copy() : ctx.contextVariables()) .triggeredByEffect(true); if (actor != null) builder.actor(actor); if (sourceItem != null) builder.sourceItem(sourceItem); - boolean result = WiredManager.handleEvent(builder.build()); + boolean result = WiredManager.dispatchEffectTriggeredEvent(builder.build()); LOGGER.debug("[SendSignal] handleEvent returned: {}", result); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java index 4f8cb4ff..35e957c4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java @@ -22,13 +22,23 @@ import gnu.trove.procedure.TObjectProcedure; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; public class WiredEffectWhisper extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.SHOW_MESSAGE; + protected static final int VISIBILITY_SOURCE_USERS = 0; + protected static final int VISIBILITY_ALL_ROOM_USERS = 1; + private static final long DELIVERY_DEDUP_TTL_MS = 60_000L; + private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512; + private static final ConcurrentHashMap DELIVERY_DEDUP = new ConcurrentHashMap<>(); protected String message = ""; protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int visibilitySelection = VISIBILITY_SOURCE_USERS; + protected int bubbleStyle = RoomChatMessageBubbles.WIRED.getType(); public WiredEffectWhisper(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -46,8 +56,10 @@ public class WiredEffectWhisper extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.message); - message.appendInt(1); + message.appendInt(3); message.appendInt(this.userSource); + message.appendInt(this.visibilitySelection); + message.appendInt(this.bubbleStyle); message.appendInt(0); message.appendInt(type.code); message.appendInt(this.getDelay()); @@ -77,6 +89,10 @@ public class WiredEffectWhisper extends InteractionWiredEffect { String message = settings.getStringParam(); int[] params = settings.getIntParams(); this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = (params.length > 1 && params[1] == VISIBILITY_ALL_ROOM_USERS) + ? VISIBILITY_ALL_ROOM_USERS + : VISIBILITY_SOURCE_USERS; + this.bubbleStyle = (params.length > 2) ? params[2] : RoomChatMessageBubbles.WIRED.getType(); if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) { message = Emulator.getGameEnvironment().getWordFilter().filter(message, null); @@ -97,17 +113,106 @@ public class WiredEffectWhisper extends InteractionWiredEffect { return WiredSourceUtil.resolveUsers(ctx, this.userSource); } + protected List resolveRecipients(WiredContext ctx, List sourceUsers) { + Room room = ctx.room(); + LinkedHashMap recipients = new LinkedHashMap<>(); + + if (room == null) { + return Collections.emptyList(); + } + + if (this.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) { + for (Habbo habbo : room.getCurrentHabbos().values()) { + addRecipient(recipients, habbo); + } + } else { + for (RoomUnit roomUnit : sourceUsers) { + addRecipient(recipients, room.getHabbo(roomUnit)); + } + } + + return new ArrayList<>(recipients.values()); + } + + protected Habbo resolveMessageSourceHabbo(WiredContext ctx, List sourceUsers) { + Room room = ctx.room(); + + if (room != null) { + for (RoomUnit roomUnit : sourceUsers) { + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) { + return habbo; + } + } + } + + return (room == null) ? null : ctx.actor().map(roomUnit -> room.getHabbo(roomUnit)).orElse(null); + } + + protected String buildMessage(WiredContext ctx, Habbo referenceHabbo) { + String username = ""; + + if (referenceHabbo != null && referenceHabbo.getHabboInfo() != null) { + username = referenceHabbo.getHabboInfo().getUsername(); + } + + String msg = this.message + .replace("%user%", username) + .replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "") + .replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""); + + return WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, msg); + } + + private void addRecipient(LinkedHashMap recipients, Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null || habbo.getClient() == null) { + return; + } + + recipients.putIfAbsent(habbo.getHabboInfo().getId(), habbo); + } + + protected boolean shouldDeliverToRecipient(WiredContext ctx, Habbo habbo) { + if (ctx == null || habbo == null || habbo.getHabboInfo() == null) { + return true; + } + + long now = System.currentTimeMillis(); + cleanupDeliveryDedup(now); + + String deliveryKey = buildDeliveryKey(ctx, habbo); + + return DELIVERY_DEDUP.putIfAbsent(deliveryKey, now) == null; + } + + private String buildDeliveryKey(WiredContext ctx, Habbo habbo) { + return ctx.room().getId() + ":" + this.getId() + ":" + habbo.getHabboInfo().getId() + ":" + ctx.event().getCreatedAtMs(); + } + + private static void cleanupDeliveryDedup(long now) { + if (DELIVERY_DEDUP.size() < DELIVERY_DEDUP_CLEANUP_THRESHOLD) { + return; + } + + DELIVERY_DEDUP.entrySet().removeIf(entry -> (now - entry.getValue()) > DELIVERY_DEDUP_TTL_MS); + } + @Override public void execute(WiredContext ctx) { - Room room = ctx.room(); if (this.message.length() > 0) { - for (RoomUnit roomUnit : resolveUsers(ctx)) { - Habbo habbo = room.getHabbo(roomUnit); - if (habbo == null) continue; + List sourceUsers = resolveUsers(ctx); + List recipients = resolveRecipients(ctx, sourceUsers); + Habbo sharedSourceHabbo = (this.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) + ? resolveMessageSourceHabbo(ctx, sourceUsers) + : null; - String msg = this.message.replace("%user%", habbo.getHabboInfo().getUsername()).replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "").replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""); - msg = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, msg); - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.WIRED))); + for (Habbo habbo : recipients) { + if (!shouldDeliverToRecipient(ctx, habbo)) { + continue; + } + + String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo); + habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.getBubble(this.bubbleStyle)))); if (habbo.getRoomUnit().isIdle()) { habbo.getRoomUnit().getRoom().unIdle(habbo); @@ -124,7 +229,7 @@ public class WiredEffectWhisper extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.message, this.getDelay(), this.userSource)); + return WiredManager.getGson().toJson(new JsonData(this.message, this.getDelay(), this.userSource, this.visibilitySelection, this.bubbleStyle)); } @Override @@ -135,7 +240,11 @@ public class WiredEffectWhisper extends InteractionWiredEffect { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); this.message = data.message; - this.userSource = data.userSource; + this.userSource = (data.userSource != null) ? data.userSource : WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = (data.visibilitySelection != null && data.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) + ? VISIBILITY_ALL_ROOM_USERS + : VISIBILITY_SOURCE_USERS; + this.bubbleStyle = (data.bubbleStyle != null) ? data.bubbleStyle : RoomChatMessageBubbles.WIRED.getType(); } else { this.message = ""; @@ -146,6 +255,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect { } this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = VISIBILITY_SOURCE_USERS; + this.bubbleStyle = RoomChatMessageBubbles.WIRED.getType(); this.needsUpdate(true); } } @@ -154,6 +265,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect { public void onPickUp() { this.message = ""; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = VISIBILITY_SOURCE_USERS; + this.bubbleStyle = RoomChatMessageBubbles.WIRED.getType(); this.setDelay(0); } @@ -170,12 +283,16 @@ public class WiredEffectWhisper extends InteractionWiredEffect { static class JsonData { String message; int delay; - int userSource; + Integer userSource; + Integer visibilitySelection; + Integer bubbleStyle; - public JsonData(String message, int delay, int userSource) { + public JsonData(String message, int delay, int userSource, int visibilitySelection, int bubbleStyle) { this.message = message; this.delay = delay; this.userSource = userSource; + this.visibilitySelection = visibilitySelection; + this.bubbleStyle = bubbleStyle; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java index e063ec88..0dd43827 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java @@ -671,8 +671,16 @@ public class WiredExtraVariableEcho extends InteractionWiredExtra { RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; - double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); - return WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), 0, true); + double targetZ = WiredUserMovementHelper.resolveUserTargetZ(room, targetTile); + return WiredUserMovementHelper.moveUser( + room, + roomUnit, + targetTile, + targetZ, + roomUnit.getBodyRotation(), + roomUnit.getHeadRotation(), + WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, + false); } private boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java index 05093e8e..d38133e4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java @@ -52,14 +52,14 @@ public class RoomChatMessageBubbles { public static final RoomChatMessageBubbles UNKNOWN_43 = new RoomChatMessageBubbles(43, "UNKNOWN_43", "", true, false); public static final RoomChatMessageBubbles UNKNOWN_44 = new RoomChatMessageBubbles(44, "UNKNOWN_44", "", true, false); public static final RoomChatMessageBubbles UNKNOWN_45 = new RoomChatMessageBubbles(45, "UNKNOWN_45", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_46 = new RoomChatMessageBubbles(45, "UNKNOWN_46", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_47 = new RoomChatMessageBubbles(45, "UNKNOWN_47", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_48 = new RoomChatMessageBubbles(45, "UNKNOWN_48", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_49 = new RoomChatMessageBubbles(45, "UNKNOWN_49", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_50 = new RoomChatMessageBubbles(45, "UNKNOWN_50", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_51 = new RoomChatMessageBubbles(45, "UNKNOWN_51", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_52 = new RoomChatMessageBubbles(45, "UNKNOWN_52", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_53 = new RoomChatMessageBubbles(45, "UNKNOWN_53", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_46 = new RoomChatMessageBubbles(46, "UNKNOWN_46", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_47 = new RoomChatMessageBubbles(47, "UNKNOWN_47", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_48 = new RoomChatMessageBubbles(48, "UNKNOWN_48", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_49 = new RoomChatMessageBubbles(49, "UNKNOWN_49", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_50 = new RoomChatMessageBubbles(50, "UNKNOWN_50", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_51 = new RoomChatMessageBubbles(51, "UNKNOWN_51", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_52 = new RoomChatMessageBubbles(52, "UNKNOWN_52", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_53 = new RoomChatMessageBubbles(53, "UNKNOWN_53", "", true, false); static { @@ -167,11 +167,11 @@ public class RoomChatMessageBubbles { public static void removeDynamicBubbles() { synchronized (BUBBLES) { - BUBBLES.entrySet().removeIf(entry -> entry.getKey() > 45); + BUBBLES.entrySet().removeIf(entry -> entry.getKey() > 53); } } public static RoomChatMessageBubbles[] values() { return BUBBLES.values().toArray(new RoomChatMessageBubbles[0]); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index 10a6f172..69b29a69 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -581,9 +581,11 @@ public final class WiredEngine { WiredMoveCarryHelper.beginMovementCollection(); - try { + try (WiredInternalVariableSupport.UserMoveBatchScope ignored = WiredInternalVariableSupport.beginUserMoveBatch()) { // Execute selected effects - for (IWiredEffect effect : toExecute) { + for (int effectIndex = 0; effectIndex < toExecute.size(); effectIndex++) { + IWiredEffect effect = toExecute.get(effectIndex); + // Check if effect requires actor if (effect.requiresActor() && !ctx.hasActor()) { continue; @@ -592,8 +594,30 @@ public final class WiredEngine { // Handle delay int delay = effect.getDelay(); if (delay > 0) { - // Schedule delayed execution - scheduleDelayedEffect(effect, ctx, delay, currentTime); + List delayedBatch = new ArrayList<>(); + delayedBatch.add(effect); + + while ((effectIndex + 1) < toExecute.size()) { + IWiredEffect nextEffect = toExecute.get(effectIndex + 1); + + if (nextEffect == null || nextEffect.getDelay() != delay) { + break; + } + + if (nextEffect.requiresActor() && !ctx.hasActor()) { + effectIndex++; + continue; + } + + delayedBatch.add(nextEffect); + effectIndex++; + } + + if (delayedBatch.size() == 1) { + scheduleDelayedEffect(effect, ctx, delay, currentTime); + } else { + scheduleOrderedEffectBatch(delayedBatch, ctx, delay, currentTime); + } } else { // Execute immediately ctx.state().step(); @@ -672,82 +696,7 @@ public final class WiredEngine { return; } - THashSet extras = room.getRoomSpecialTypes().getExtras( - stack.triggerItem().getX(), - stack.triggerItem().getY()); - - if (extras == null || extras.isEmpty()) { - return; - } - - int furniLimit = Integer.MAX_VALUE; - int userLimit = Integer.MAX_VALUE; - List furniVariableFilters = new ArrayList<>(); - List userVariableFilters = new ArrayList<>(); - - for (InteractionWiredExtra extra : extras) { - if (extra instanceof WiredExtraFilterFurni) { - furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); - } else if (extra instanceof WiredExtraFilterUser) { - userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); - } else if (extra instanceof WiredExtraFilterFurniByVariable) { - furniVariableFilters.add((WiredExtraFilterFurniByVariable) extra); - } else if (extra instanceof WiredExtraFilterUsersByVariable) { - userVariableFilters.add((WiredExtraFilterUsersByVariable) extra); - } - } - - furniVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); - userVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); - - if (ctx.targets().isItemsModifiedBySelector()) { - Iterable filteredItems = ctx.targets().items(); - - for (WiredExtraFilterFurniByVariable extra : furniVariableFilters) { - filteredItems = extra.filterItems(room, ctx, filteredItems); - } - - if (furniLimit != Integer.MAX_VALUE) { - filteredItems = limitIterable(filteredItems, furniLimit); - } - - ctx.targets().setItems(filteredItems); - } - - if (ctx.targets().isUsersModifiedBySelector()) { - Iterable filteredUsers = ctx.targets().users(); - - for (WiredExtraFilterUsersByVariable extra : userVariableFilters) { - filteredUsers = extra.filterUsers(room, ctx, filteredUsers); - } - - if (userLimit != Integer.MAX_VALUE) { - filteredUsers = limitIterable(filteredUsers, userLimit); - } - - ctx.targets().setUsers(filteredUsers); - } - } - - private List limitIterable(Iterable values, int limit) { - List result = new ArrayList<>(); - - if (values == null || limit <= 0) { - return result; - } - - for (T value : values) { - if (value != null) { - result.add(value); - } - } - - if (result.size() <= limit) { - return result; - } - - Collections.shuffle(result, Emulator.getRandom()); - return new ArrayList<>(result.subList(0, limit)); + WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx); } /** @@ -916,7 +865,7 @@ public final class WiredEngine { WiredMoveCarryHelper.beginMovementCollection(); - try { + try (WiredInternalVariableSupport.UserMoveBatchScope ignored = WiredInternalVariableSupport.beginUserMoveBatch()) { for (IWiredEffect effect : batch) { try { if (!useExecutionTimeForCooldown) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java index c1fe88b4..24f70935 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java @@ -26,6 +26,10 @@ import java.time.temporal.WeekFields; import java.util.Locale; public final class WiredInternalVariableSupport { + private static final ThreadLocal USER_MOVE_INSTANT_OVERRIDE = new ThreadLocal<>(); + private static final ThreadLocal USER_MOVE_BATCH = new ThreadLocal<>(); + private static final ThreadLocal USER_MOVE_BATCH_DEPTH = new ThreadLocal<>(); + private WiredInternalVariableSupport() { } @@ -225,15 +229,29 @@ public final class WiredInternalVariableSupport { } public static boolean writeUserValue(Room room, RoomUnit roomUnit, String key, int value) { + Boolean instantOverride = USER_MOVE_INSTANT_OVERRIDE.get(); + + if (instantOverride != null) { + return writeUserValue(room, roomUnit, key, value, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, instantOverride); + } + + return writeUserValue(room, roomUnit, key, value, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, false); + } + + public static boolean writeUserValue(Room room, RoomUnit roomUnit, String key, int value, int animationDuration, boolean noAnimation) { if (room == null || roomUnit == null) { return false; } String normalized = normalizeKey(key); + if (stageUserMoveIfPossible(room, roomUnit, normalized, value, animationDuration, noAnimation)) { + return true; + } + return switch (normalized) { - case "@position_x" -> moveUserTo(room, roomUnit, value, roomUnit.getY()); - case "@position_y" -> moveUserTo(room, roomUnit, roomUnit.getX(), value); + case "@position_x" -> moveUserTo(room, roomUnit, value, roomUnit.getY(), animationDuration, noAnimation); + case "@position_y" -> moveUserTo(room, roomUnit, roomUnit.getX(), value, animationDuration, noAnimation); case "@direction" -> { RoomUserRotation rotation = RoomUserRotation.fromValue(value); yield WiredUserMovementHelper.updateUserDirection(room, roomUnit, rotation, rotation); @@ -242,6 +260,24 @@ public final class WiredInternalVariableSupport { }; } + public static UserMoveInstantScope beginUserMoveInstantOverride(boolean instant) { + Boolean previousValue = USER_MOVE_INSTANT_OVERRIDE.get(); + USER_MOVE_INSTANT_OVERRIDE.set(instant); + return new UserMoveInstantScope(previousValue); + } + + public static UserMoveBatchScope beginUserMoveBatch() { + Integer previousDepth = USER_MOVE_BATCH_DEPTH.get(); + int nextDepth = (previousDepth == null) ? 1 : (previousDepth + 1); + USER_MOVE_BATCH_DEPTH.set(nextDepth); + + if (nextDepth == 1) { + USER_MOVE_BATCH.set(new UserMoveBatch()); + } + + return new UserMoveBatchScope(previousDepth); + } + public static Integer readFurniValue(Room room, HabboItem item, String key) { if (room == null || item == null) { return null; @@ -281,7 +317,7 @@ public final class WiredInternalVariableSupport { String normalized = normalizeKey(key); if ("@state".equals(normalized)) { - item.setExtradata(String.valueOf(value)); + item.setExtradata(String.valueOf(normalizeFurniStateValue(item, value))); room.updateItemState(item); return true; } @@ -492,7 +528,7 @@ public final class WiredInternalVariableSupport { return parseInteger(roomUnit.getStatus(status)); } - private static boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y) { + private static boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y, int animationDuration, boolean noAnimation) { if (room == null || roomUnit == null || room.getLayout() == null) { return false; } @@ -502,8 +538,95 @@ public final class WiredInternalVariableSupport { return false; } - double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); - return WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), 0, true); + double targetZ = WiredUserMovementHelper.resolveUserTargetZ(room, targetTile); + return WiredUserMovementHelper.moveUser( + room, + roomUnit, + targetTile, + targetZ, + roomUnit.getBodyRotation(), + roomUnit.getHeadRotation(), + animationDuration, + noAnimation); + } + + private static boolean stageUserMoveIfPossible(Room room, RoomUnit roomUnit, String normalizedKey, int value, int animationDuration, boolean noAnimation) { + if (room == null || roomUnit == null || normalizedKey == null) { + return false; + } + + if (!"@position_x".equals(normalizedKey) && !"@position_y".equals(normalizedKey)) { + return false; + } + + UserMoveBatch batch = USER_MOVE_BATCH.get(); + + if (batch == null) { + return false; + } + + UserMoveBatchEntry entry = batch.entries.computeIfAbsent(roomUnit.getId(), ignored -> + new UserMoveBatchEntry(room, roomUnit, roomUnit.getX(), roomUnit.getY(), animationDuration, noAnimation)); + + entry.animationDuration = animationDuration; + entry.noAnimation = noAnimation; + + if ("@position_x".equals(normalizedKey)) { + entry.targetX = value; + entry.xDirty = true; + } else { + entry.targetY = value; + entry.yDirty = true; + } + + if (entry.xDirty && entry.yDirty && !entry.noAnimation) { + executeUserMoveBatchEntry(entry); + } + + return true; + } + + private static void flushUserMoveBatch(UserMoveBatch batch) { + if (batch == null || batch.entries.isEmpty()) { + return; + } + + for (UserMoveBatchEntry entry : batch.entries.values()) { + executeUserMoveBatchEntry(entry); + } + } + + private static void executeUserMoveBatchEntry(UserMoveBatchEntry entry) { + if (entry == null || entry.room == null || entry.roomUnit == null || entry.room.getLayout() == null) { + return; + } + + if (!entry.xDirty && !entry.yDirty) { + return; + } + + RoomTile targetTile = entry.room.getLayout().getTile((short) entry.targetX, (short) entry.targetY); + + if (targetTile == null || targetTile.state == RoomTileState.INVALID) { + return; + } + + double targetZ = WiredUserMovementHelper.resolveUserTargetZ(entry.room, targetTile); + + WiredUserMovementHelper.moveUser( + entry.room, + entry.roomUnit, + targetTile, + targetZ, + entry.roomUnit.getBodyRotation(), + entry.roomUnit.getHeadRotation(), + entry.animationDuration, + entry.noAnimation); + + entry.targetX = entry.roomUnit.getX(); + entry.targetY = entry.roomUnit.getY(); + entry.xDirty = false; + entry.yDirty = false; } private static boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { @@ -520,6 +643,24 @@ public final class WiredInternalVariableSupport { return error == FurnitureMovementError.NONE; } + private static int normalizeFurniStateValue(HabboItem item, int value) { + if (item == null || item.getBaseItem() == null) { + return value; + } + + int stateCount = item.getBaseItem().getStateCount(); + if (stateCount <= 0) { + return value; + } + + int wrappedValue = value % stateCount; + if (wrappedValue < 0) { + wrappedValue += stateCount; + } + + return wrappedValue; + } + private static int parseInteger(String value) { try { return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); @@ -551,4 +692,96 @@ public final class WiredInternalVariableSupport { this.typeId = typeId; } } + + public static final class UserMoveInstantScope implements AutoCloseable { + private final Boolean previousValue; + private boolean closed; + + private UserMoveInstantScope(Boolean previousValue) { + this.previousValue = previousValue; + } + + @Override + public void close() { + if (this.closed) { + return; + } + + this.closed = true; + + if (this.previousValue == null) { + USER_MOVE_INSTANT_OVERRIDE.remove(); + return; + } + + USER_MOVE_INSTANT_OVERRIDE.set(this.previousValue); + } + } + + public static final class UserMoveBatchScope implements AutoCloseable { + private final Integer previousDepth; + private boolean closed; + + private UserMoveBatchScope(Integer previousDepth) { + this.previousDepth = previousDepth; + } + + @Override + public void close() { + if (this.closed) { + return; + } + + this.closed = true; + + Integer currentDepth = USER_MOVE_BATCH_DEPTH.get(); + + if (currentDepth == null || currentDepth <= 1) { + UserMoveBatch currentBatch = USER_MOVE_BATCH.get(); + + if (currentBatch != null) { + flushUserMoveBatch(currentBatch); + } + + USER_MOVE_BATCH.remove(); + + if (this.previousDepth == null) { + USER_MOVE_BATCH_DEPTH.remove(); + } else { + USER_MOVE_BATCH_DEPTH.set(this.previousDepth); + } + + return; + } + + USER_MOVE_BATCH_DEPTH.set(currentDepth - 1); + } + } + + private static final class UserMoveBatch { + private final java.util.LinkedHashMap entries = new java.util.LinkedHashMap<>(); + } + + private static final class UserMoveBatchEntry { + private final Room room; + private final RoomUnit roomUnit; + private int targetX; + private int targetY; + private int animationDuration; + private boolean noAnimation; + private boolean xDirty; + private boolean yDirty; + + private UserMoveBatchEntry(Room room, RoomUnit roomUnit, int targetX, int targetY, int animationDuration, boolean noAnimation) { + this.room = room; + this.roomUnit = roomUnit; + this.targetX = targetX; + this.targetY = targetY; + this.animationDuration = animationDuration; + this.noAnimation = noAnimation; + this.xDirty = false; + this.yDirty = false; + } + } + } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java index 9d264065..8949e0d8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java @@ -39,6 +39,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayDeque; /** * Manager class for the wired runtime. @@ -85,6 +86,8 @@ public final class WiredManager { /** Whether the engine is initialized */ private static volatile boolean initialized = false; + private static final ThreadLocal EVENT_HANDLING_DEPTH = new ThreadLocal<>(); + private static final ThreadLocal> DEFERRED_EFFECT_EVENTS = new ThreadLocal<>(); private WiredManager() { // Static utility class } @@ -236,8 +239,65 @@ public final class WiredManager { if (event == null || RoomWiredDisableSupport.isWiredDisabled(event.getRoom())) { return false; } - - return engine.handleEvent(event); + + Integer previousDepth = EVENT_HANDLING_DEPTH.get(); + int nextDepth = (previousDepth == null) ? 1 : (previousDepth + 1); + EVENT_HANDLING_DEPTH.set(nextDepth); + + if (previousDepth == null) { + DEFERRED_EFFECT_EVENTS.set(new ArrayDeque<>()); + } + + boolean handled = false; + + try { + handled = engine.handleEvent(event); + + if (nextDepth == 1) { + ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); + + while (deferredEvents != null && !deferredEvents.isEmpty()) { + WiredEvent deferredEvent = deferredEvents.pollFirst(); + + if (deferredEvent == null || RoomWiredDisableSupport.isWiredDisabled(deferredEvent.getRoom())) { + continue; + } + + handled = engine.handleEvent(deferredEvent) || handled; + } + } + + return handled; + } finally { + if (previousDepth == null) { + EVENT_HANDLING_DEPTH.remove(); + DEFERRED_EFFECT_EVENTS.remove(); + } else { + EVENT_HANDLING_DEPTH.set(previousDepth); + } + } + } + + public static boolean dispatchEffectTriggeredEvent(WiredEvent event) { + if (!isEnabled() || engine == null || event == null || RoomWiredDisableSupport.isWiredDisabled(event.getRoom())) { + return false; + } + + Integer currentDepth = EVENT_HANDLING_DEPTH.get(); + + if (currentDepth == null || currentDepth <= 0) { + return handleEvent(event); + } + + ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); + + if (deferredEvents == null) { + deferredEvents = new ArrayDeque<>(); + DEFERRED_EFFECT_EVENTS.set(deferredEvents); + } + + deferredEvents.addLast(event); + return true; } /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java index 5647a45e..813b3f15 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java @@ -35,6 +35,7 @@ public final class WiredMoveCarryHelper { private static final long USER_FOLLOWER_TTL_MS = 10000L; private static final ThreadLocal> SUPPRESSED_STATUS_ROOM_UNIT_IDS = new ThreadLocal<>(); private static final ThreadLocal> COLLECTED_MOVEMENTS = new ThreadLocal<>(); + private static final ThreadLocal MOVEMENT_COLLECTION_DEPTH = new ThreadLocal<>(); private static final ConcurrentHashMap SUPPRESSED_STATUS_COMPOSER_UNTIL = new ConcurrentHashMap<>(); private static final ConcurrentHashMap> ACTIVE_USER_FOLLOWERS = new ConcurrentHashMap<>(); @@ -237,12 +238,28 @@ public final class WiredMoveCarryHelper { } public static void beginMovementCollection() { - COLLECTED_MOVEMENTS.set(new ArrayList<>()); + Integer currentDepth = MOVEMENT_COLLECTION_DEPTH.get(); + + if (currentDepth == null || currentDepth <= 0) { + COLLECTED_MOVEMENTS.set(new ArrayList<>()); + MOVEMENT_COLLECTION_DEPTH.set(1); + return; + } + + MOVEMENT_COLLECTION_DEPTH.set(currentDepth + 1); } public static ServerMessage finishMovementCollection() { + Integer currentDepth = MOVEMENT_COLLECTION_DEPTH.get(); + + if (currentDepth != null && currentDepth > 1) { + MOVEMENT_COLLECTION_DEPTH.set(currentDepth - 1); + return null; + } + List movements = COLLECTED_MOVEMENTS.get(); COLLECTED_MOVEMENTS.remove(); + MOVEMENT_COLLECTION_DEPTH.remove(); if (movements == null || movements.isEmpty()) { return null; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSelectionFilterSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSelectionFilterSupport.java new file mode 100644 index 00000000..306e71b5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSelectionFilterSupport.java @@ -0,0 +1,204 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class WiredSelectionFilterSupport { + private static final ThreadLocal FILTER_DEPTH = ThreadLocal.withInitial(() -> 0); + + private WiredSelectionFilterSupport() { + } + + static void applySelectorFilters(Room room, HabboItem triggerItem, WiredContext ctx) { + if (ctx == null) { + return; + } + + if (ctx.targets().isItemsModifiedBySelector()) { + ctx.targets().setItems(filterItems(room, triggerItem, ctx, ctx.targets().items())); + } + + if (ctx.targets().isUsersModifiedBySelector()) { + ctx.targets().setUsers(filterUsers(room, triggerItem, ctx, ctx.targets().users())); + } + } + + static List filterItems(Room room, HabboItem triggerItem, WiredContext ctx, Iterable values) { + List items = toItemList(values); + + if (items.isEmpty() || shouldBypass(room, triggerItem, ctx)) { + return items; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); + if (extras == null || extras.isEmpty()) { + return items; + } + + int furniLimit = Integer.MAX_VALUE; + List variableFilters = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraFilterFurni) { + furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); + } else if (extra instanceof WiredExtraFilterFurniByVariable) { + variableFilters.add((WiredExtraFilterFurniByVariable) extra); + } + } + + if (furniLimit == Integer.MAX_VALUE && variableFilters.isEmpty()) { + return items; + } + + variableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + + try (FilterScope ignored = enterScope()) { + Iterable filteredItems = items; + + for (WiredExtraFilterFurniByVariable extra : variableFilters) { + filteredItems = extra.filterItems(room, ctx, filteredItems); + } + + if (furniLimit != Integer.MAX_VALUE) { + filteredItems = limitIterable(filteredItems, furniLimit); + } + + return toItemList(filteredItems); + } + } + + static List filterUsers(Room room, HabboItem triggerItem, WiredContext ctx, Iterable values) { + List users = toUserList(values); + + if (users.isEmpty() || shouldBypass(room, triggerItem, ctx)) { + return users; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); + if (extras == null || extras.isEmpty()) { + return users; + } + + int userLimit = Integer.MAX_VALUE; + List variableFilters = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraFilterUser) { + userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); + } else if (extra instanceof WiredExtraFilterUsersByVariable) { + variableFilters.add((WiredExtraFilterUsersByVariable) extra); + } + } + + if (userLimit == Integer.MAX_VALUE && variableFilters.isEmpty()) { + return users; + } + + variableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + + try (FilterScope ignored = enterScope()) { + Iterable filteredUsers = users; + + for (WiredExtraFilterUsersByVariable extra : variableFilters) { + filteredUsers = extra.filterUsers(room, ctx, filteredUsers); + } + + if (userLimit != Integer.MAX_VALUE) { + filteredUsers = limitIterable(filteredUsers, userLimit); + } + + return toUserList(filteredUsers); + } + } + + private static boolean shouldBypass(Room room, HabboItem triggerItem, WiredContext ctx) { + return room == null + || triggerItem == null + || ctx == null + || room.getRoomSpecialTypes() == null + || FILTER_DEPTH.get() > 0; + } + + private static FilterScope enterScope() { + FILTER_DEPTH.set(FILTER_DEPTH.get() + 1); + return new FilterScope(); + } + + private static List limitIterable(Iterable values, int limit) { + List result = new ArrayList<>(); + + if (values == null || limit <= 0) { + return result; + } + + for (T value : values) { + if (value != null) { + result.add(value); + } + } + + if (result.size() <= limit) { + return result; + } + + Collections.shuffle(result, Emulator.getRandom()); + return new ArrayList<>(result.subList(0, limit)); + } + + private static List toItemList(Iterable values) { + List result = new ArrayList<>(); + + if (values == null) { + return result; + } + + for (HabboItem item : values) { + if (item != null) { + result.add(item); + } + } + + return result; + } + + private static List toUserList(Iterable values) { + List result = new ArrayList<>(); + + if (values == null) { + return result; + } + + for (RoomUnit unit : values) { + if (unit != null) { + result.add(unit); + } + } + + return result; + } + + private static final class FilterScope implements AutoCloseable { + @Override + public void close() { + int depth = FILTER_DEPTH.get() - 1; + + if (depth <= 0) { + FILTER_DEPTH.remove(); + } else { + FILTER_DEPTH.set(depth); + } + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java index a400076c..671ec052 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java @@ -29,24 +29,36 @@ public final class WiredSourceUtil { } public static List resolveItems(WiredContext ctx, int sourceType, Collection selectedItems) { + List resolvedItems; + switch (sourceType) { case SOURCE_TRIGGER: - return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + resolvedItems = ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + break; case SOURCE_SELECTED: - return (selectedItems != null) ? new ArrayList<>(selectedItems) : Collections.emptyList(); + resolvedItems = (selectedItems != null) ? new ArrayList<>(selectedItems) : Collections.emptyList(); + break; case SOURCE_SELECTOR: WiredTargets itemTargets = getSelectorTargets(ctx); - return itemTargets.isItemsModifiedBySelector() + resolvedItems = itemTargets.isItemsModifiedBySelector() ? new ArrayList<>(itemTargets.items()) : Collections.emptyList(); + break; case SOURCE_SIGNAL: if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { - return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + resolvedItems = ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + break; } - return Collections.emptyList(); + resolvedItems = Collections.emptyList(); + break; default: - return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + resolvedItems = ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + break; } + + return (sourceType == SOURCE_SELECTOR) + ? resolvedItems + : WiredSelectionFilterSupport.filterItems(ctx.room(), ctx.triggerItem(), ctx, resolvedItems); } public static List resolveUsers(WiredContext ctx, int sourceType) { @@ -54,29 +66,43 @@ public final class WiredSourceUtil { } public static List resolveUsers(WiredContext ctx, int sourceType, Collection selectedUsers) { + List resolvedUsers; + switch (sourceType) { case SOURCE_TRIGGER: - return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + resolvedUsers = ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + break; case SOURCE_CLICKED_USER: if (ctx.eventType() == WiredEvent.Type.USER_CLICKS_USER) { - return ctx.event().getTargetUnit().map(Collections::singletonList).orElse(Collections.emptyList()); + resolvedUsers = ctx.event().getTargetUnit().map(Collections::singletonList).orElse(Collections.emptyList()); + break; } - return Collections.emptyList(); + resolvedUsers = Collections.emptyList(); + break; case SOURCE_SELECTED: - return (selectedUsers != null) ? new ArrayList<>(selectedUsers) : Collections.emptyList(); + resolvedUsers = (selectedUsers != null) ? new ArrayList<>(selectedUsers) : Collections.emptyList(); + break; case SOURCE_SELECTOR: WiredTargets userTargets = getSelectorTargets(ctx); - return userTargets.isUsersModifiedBySelector() + resolvedUsers = userTargets.isUsersModifiedBySelector() ? new ArrayList<>(userTargets.users()) : Collections.emptyList(); + break; case SOURCE_SIGNAL: if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { - return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + resolvedUsers = ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + break; } - return Collections.emptyList(); + resolvedUsers = Collections.emptyList(); + break; default: - return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + resolvedUsers = ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + break; } + + return (sourceType == SOURCE_SELECTOR) + ? resolvedUsers + : WiredSelectionFilterSupport.filterUsers(ctx.room(), ctx.triggerItem(), ctx, resolvedUsers); } public static boolean isDefaultUserSource(int value) { @@ -223,83 +249,6 @@ public final class WiredSourceUtil { } private static void applySelectionFilterExtras(Room room, HabboItem triggerItem, WiredContext selectorCtx) { - if (room == null || triggerItem == null || selectorCtx == null || room.getRoomSpecialTypes() == null) { - return; - } - - THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); - - if (extras == null || extras.isEmpty()) { - return; - } - - int furniLimit = Integer.MAX_VALUE; - int userLimit = Integer.MAX_VALUE; - List furniVariableFilters = new ArrayList<>(); - List userVariableFilters = new ArrayList<>(); - - for (InteractionWiredExtra extra : extras) { - if (extra instanceof WiredExtraFilterFurni) { - furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); - } else if (extra instanceof WiredExtraFilterUser) { - userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); - } else if (extra instanceof WiredExtraFilterFurniByVariable) { - furniVariableFilters.add((WiredExtraFilterFurniByVariable) extra); - } else if (extra instanceof WiredExtraFilterUsersByVariable) { - userVariableFilters.add((WiredExtraFilterUsersByVariable) extra); - } - } - - furniVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); - userVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); - - if (selectorCtx.targets().isItemsModifiedBySelector()) { - Iterable filteredItems = selectorCtx.targets().items(); - - for (WiredExtraFilterFurniByVariable extra : furniVariableFilters) { - filteredItems = extra.filterItems(room, selectorCtx, filteredItems); - } - - if (furniLimit != Integer.MAX_VALUE) { - filteredItems = limitIterable(filteredItems, furniLimit); - } - - selectorCtx.targets().setItems(filteredItems); - } - - if (selectorCtx.targets().isUsersModifiedBySelector()) { - Iterable filteredUsers = selectorCtx.targets().users(); - - for (WiredExtraFilterUsersByVariable extra : userVariableFilters) { - filteredUsers = extra.filterUsers(room, selectorCtx, filteredUsers); - } - - if (userLimit != Integer.MAX_VALUE) { - filteredUsers = limitIterable(filteredUsers, userLimit); - } - - selectorCtx.targets().setUsers(filteredUsers); - } - } - - private static List limitIterable(Iterable values, int limit) { - List result = new ArrayList<>(); - - if (values == null || limit <= 0) { - return result; - } - - for (T value : values) { - if (value != null) { - result.add(value); - } - } - - if (result.size() <= limit) { - return result; - } - - Collections.shuffle(result, Emulator.getRandom()); - return new ArrayList<>(result.subList(0, limit)); + WiredSelectionFilterSupport.applySelectorFilters(room, triggerItem, selectorCtx); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java index 4e7d3ecb..15c50f03 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java @@ -162,82 +162,6 @@ public final class WiredTriggerSourceUtil { } private static void applySelectionFilterExtras(Room room, HabboItem triggerItem, WiredContext selectorCtx) { - if (room == null || triggerItem == null || selectorCtx == null || room.getRoomSpecialTypes() == null) { - return; - } - - THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); - - if (extras == null || extras.isEmpty()) { - return; - } - - int furniLimit = Integer.MAX_VALUE; - int userLimit = Integer.MAX_VALUE; - List furniVariableFilters = new ArrayList<>(); - List userVariableFilters = new ArrayList<>(); - - for (InteractionWiredExtra extra : extras) { - if (extra instanceof WiredExtraFilterFurni) { - furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); - } else if (extra instanceof WiredExtraFilterUser) { - userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); - } else if (extra instanceof WiredExtraFilterFurniByVariable) { - furniVariableFilters.add((WiredExtraFilterFurniByVariable) extra); - } else if (extra instanceof WiredExtraFilterUsersByVariable) { - userVariableFilters.add((WiredExtraFilterUsersByVariable) extra); - } - } - - furniVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); - userVariableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); - - if (selectorCtx.targets().isItemsModifiedBySelector()) { - Iterable filteredItems = selectorCtx.targets().items(); - - for (WiredExtraFilterFurniByVariable extra : furniVariableFilters) { - filteredItems = extra.filterItems(room, selectorCtx, filteredItems); - } - - if (furniLimit != Integer.MAX_VALUE) { - filteredItems = limitIterable(filteredItems, furniLimit); - } - - selectorCtx.targets().setItems(filteredItems); - } - - if (selectorCtx.targets().isUsersModifiedBySelector()) { - Iterable filteredUsers = selectorCtx.targets().users(); - - for (WiredExtraFilterUsersByVariable extra : userVariableFilters) { - filteredUsers = extra.filterUsers(room, selectorCtx, filteredUsers); - } - - if (userLimit != Integer.MAX_VALUE) { - filteredUsers = limitIterable(filteredUsers, userLimit); - } - - selectorCtx.targets().setUsers(filteredUsers); - } - } - - private static List limitIterable(Iterable values, int limit) { - List result = new ArrayList<>(); - - if (values == null || limit <= 0) { - return result; - } - - for (T value : values) { - if (value != null) { - result.add(value); - } - } - - if (result.size() <= limit) { - return result; - } - - return new ArrayList<>(result.subList(0, limit)); + WiredSelectionFilterSupport.applySelectorFilters(room, triggerItem, selectorCtx); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java index 2ead572e..a08a64ba 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java @@ -1,8 +1,13 @@ package com.eu.habbo.habbohotel.wired.core; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; +import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; +import com.eu.habbo.habbohotel.items.interactions.interfaces.ConditionalGate; import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomLayout; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; @@ -17,6 +22,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; @@ -26,7 +32,9 @@ import java.util.concurrent.ConcurrentHashMap; public final class WiredUserMovementHelper { public static final int DEFAULT_ANIMATION_DURATION = WiredMovementsComposer.DEFAULT_DURATION; + private static final int SUPPRESS_NEXT_WALK_WINDOW_MS = 250; private static final int STATUS_SUPPRESSION_GRACE_MS = 250; + private static final String SUPPRESS_NEXT_WALK_CACHE_KEY = "wired_suppress_next_walk_until"; private static final Logger LOGGER = LoggerFactory.getLogger(WiredUserMovementHelper.class); private static final ThreadLocal> SUPPRESSED_STATUS_ROOM_UNIT_IDS = new ThreadLocal<>(); @@ -62,16 +70,29 @@ public final class WiredUserMovementHelper { RoomUserRotation resolvedBodyRotation = bodyRotation == null ? roomUnit.getBodyRotation() : bodyRotation; RoomUserRotation resolvedHeadRotation = headRotation == null ? roomUnit.getHeadRotation() : headRotation; - double oldZ = roomUnit.getZ(); - HabboItem oldTopItem = room.getTopItemAt(oldLocation.x, oldLocation.y); - HabboItem newTopItem = room.getTopItemAt(targetTile.x, targetTile.y); - Habbo habbo = room.getHabbo(roomUnit); - int animationDuration = Math.max(1, duration); if (noAnimation) { - return moveUserInstant(room, roomUnit, targetTile, targetZ, resolvedBodyRotation, resolvedHeadRotation, oldLocation, oldTopItem, newTopItem, habbo); + return moveUserInstant( + room, + roomUnit, + oldLocation, + targetTile, + roomUnit.getZ(), + targetZ, + resolvedBodyRotation, + resolvedHeadRotation, + room.getTopItemAt(oldLocation.x, oldLocation.y), + resolveEnteredItem(room, targetTile), + room.getHabbo(roomUnit)); } + double oldZ = roomUnit.getZ(); + HabboItem oldTopItem = room.getTopItemAt(oldLocation.x, oldLocation.y); + HabboItem newTopItem = resolveEnteredItem(room, targetTile); + Habbo habbo = room.getHabbo(roomUnit); + + int animationDuration = (duration > 0) ? duration : DEFAULT_ANIMATION_DURATION; + runWithSuppressedStatusUpdates(Collections.singletonList(roomUnit), () -> { roomUnit.removeStatus(RoomUnitStatus.MOVE); roomUnit.setZ(targetZ); @@ -111,6 +132,61 @@ public final class WiredUserMovementHelper { return true; } + public static void suppressNextWalkCommand(RoomUnit roomUnit) { + if (roomUnit == null || roomUnit.getCacheable() == null) { + return; + } + + roomUnit.getCacheable().put(SUPPRESS_NEXT_WALK_CACHE_KEY, System.currentTimeMillis() + SUPPRESS_NEXT_WALK_WINDOW_MS); + } + + public static boolean consumeSuppressedWalkCommand(RoomUnit roomUnit) { + if (roomUnit == null || roomUnit.getCacheable() == null) { + return false; + } + + Object value = roomUnit.getCacheable().remove(SUPPRESS_NEXT_WALK_CACHE_KEY); + + if (!(value instanceof Long)) { + return false; + } + + return ((Long) value) >= System.currentTimeMillis(); + } + + private static boolean moveUserInstant(Room room, RoomUnit roomUnit, RoomTile oldLocation, RoomTile targetTile, double oldZ, double targetZ, + RoomUserRotation bodyRotation, RoomUserRotation headRotation, HabboItem oldTopItem, + HabboItem newTopItem, Habbo habbo) { + suppressNextWalkCommand(roomUnit); + roomUnit.removeStatus(RoomUnitStatus.MOVE); + roomUnit.setPath(new LinkedList<>()); + roomUnit.setBodyRotation(bodyRotation); + roomUnit.setHeadRotation(headRotation); + roomUnit.setCurrentLocation(targetTile); + roomUnit.setGoalLocation(targetTile); + roomUnit.setZ(targetZ); + roomUnit.setPreviousLocation(oldLocation); + roomUnit.setPreviousLocationZ(oldZ); + roomUnit.resetIdleTimer(); + roomUnit.statusUpdate(true); + + if (habbo != null) { + THashSet movedHabbos = new THashSet<>(); + movedHabbos.add(habbo); + room.updateHabbosAt(targetTile.x, targetTile.y, movedHabbos); + } else { + switch (roomUnit.getRoomUnitType()) { + case BOT -> room.updateBotsAt(targetTile.x, targetTile.y); + case PET -> room.updatePetsAt(targetTile.x, targetTile.y); + } + } + + processTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem); + clearStatusComposerSuppression(roomUnit); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + return true; + } + public static boolean updateUserDirection(Room room, RoomUnit roomUnit, RoomUserRotation bodyRotation, RoomUserRotation headRotation) { if (room == null || roomUnit == null) { return false; @@ -244,39 +320,10 @@ public final class WiredUserMovementHelper { return hasIgnoredFurni; } - private static boolean moveUserInstant(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, RoomTile oldLocation, HabboItem oldTopItem, HabboItem newTopItem, Habbo habbo) { - runWithSuppressedStatusUpdates(Collections.singletonList(roomUnit), () -> { - roomUnit.removeStatus(RoomUnitStatus.MOVE); - roomUnit.setZ(targetZ); - roomUnit.setLocation(targetTile); - roomUnit.setPath(new LinkedList<>()); - roomUnit.setBodyRotation(bodyRotation); - roomUnit.setHeadRotation(headRotation); - roomUnit.resetIdleTimer(); - - if (habbo != null) { - THashSet movedHabbos = new THashSet<>(); - movedHabbos.add(habbo); - room.updateHabbosAt(targetTile.x, targetTile.y, movedHabbos); - } - roomUnit.statusUpdate(false); - }); - - processTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem); - roomUnit.setPreviousLocation(roomUnit.getCurrentLocation()); - roomUnit.setPreviousLocationZ(roomUnit.getZ()); - room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); - return true; - } - private static void scheduleTileCallbacks(Room room, RoomUnit roomUnit, RoomTile oldLocation, RoomTile targetTile, HabboItem oldTopItem, HabboItem newTopItem, int delay) { - if (oldTopItem == null && newTopItem == null) { - return; - } - Emulator.getThreading().run(() -> { processTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem); - }, Math.max(delay, InteractionRoller.DELAY)); + }, Math.max(delay, 1)); } private static void processTileCallbacks(Room room, RoomUnit roomUnit, RoomTile oldLocation, RoomTile targetTile, HabboItem oldTopItem, HabboItem newTopItem) { @@ -288,7 +335,15 @@ public final class WiredUserMovementHelper { return; } - if (oldTopItem != null && oldTopItem != newTopItem) { + HabboItem resolvedNewTopItem = resolveEnteredItem(room, targetTile); + if (resolvedNewTopItem == null) { + resolvedNewTopItem = room.getTopItemAt(targetTile.x, targetTile.y); + } + if (resolvedNewTopItem == null) { + resolvedNewTopItem = newTopItem; + } + + if (oldTopItem != null && (oldTopItem != resolvedNewTopItem || !occupiesTile(oldTopItem, targetTile))) { try { oldTopItem.onWalkOff(roomUnit, room, new Object[]{oldLocation, targetTile}); } catch (Exception exception) { @@ -296,13 +351,155 @@ public final class WiredUserMovementHelper { } } - if (newTopItem != null && newTopItem != oldTopItem) { + for (HabboItem additionalOldItem : resolveAdditionalTileItems(room, oldLocation, oldTopItem)) { + if (additionalOldItem == resolvedNewTopItem && occupiesTile(additionalOldItem, targetTile)) { + continue; + } + try { - newTopItem.onWalkOn(roomUnit, room, new Object[]{oldLocation, targetTile}); + additionalOldItem.onWalkOff(roomUnit, room, new Object[]{oldLocation, targetTile}); + } catch (Exception exception) { + LOGGER.error("Failed to process additional wired user walk off callback", exception); + } + } + + if (resolvedNewTopItem != null) { + try { + if (resolvedNewTopItem != oldTopItem || !occupiesTile(resolvedNewTopItem, oldLocation)) { + if (!resolvedNewTopItem.canWalkOn(roomUnit, room, null)) { + if (resolvedNewTopItem instanceof ConditionalGate) { + roomUnit.setLocation(oldLocation); + roomUnit.setZ(oldLocation.getStackHeight()); + roomUnit.setPreviousLocation(oldLocation); + roomUnit.setPreviousLocationZ(oldLocation.getStackHeight()); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + return; + } + } else { + resolvedNewTopItem.onWalkOn(roomUnit, room, new Object[]{oldLocation, targetTile}); + } + } else { + resolvedNewTopItem.onWalk(roomUnit, room, new Object[]{oldLocation, targetTile}); + } } catch (Exception exception) { LOGGER.error("Failed to process wired user walk on callback", exception); } } + + for (HabboItem additionalNewItem : resolveAdditionalTileItems(room, targetTile, resolvedNewTopItem)) { + try { + if (occupiesTile(additionalNewItem, oldLocation)) { + additionalNewItem.onWalk(roomUnit, room, new Object[]{oldLocation, targetTile}); + } else { + additionalNewItem.onWalkOn(roomUnit, room, new Object[]{oldLocation, targetTile}); + } + } catch (Exception exception) { + LOGGER.error("Failed to process additional wired user walk on callback", exception); + } + } + } + + private static HabboItem resolveEnteredItem(Room room, RoomTile tile) { + if (room == null || tile == null || room.getItemManager() == null) { + return null; + } + + if (room.canSitAt(tile.x, tile.y)) { + HabboItem tallestChair = room.getTallestChair(tile); + if (tallestChair != null) { + return tallestChair; + } + } + + HabboItem candidate = null; + + for (HabboItem item : room.getItemsAt(tile)) { + if (item == null || !occupiesTile(item, tile)) { + continue; + } + + boolean preferred = item instanceof ConditionalGate + || item.isWalkable() + || item.getBaseItem().allowWalk() + || item.getBaseItem().allowSit() + || item.getBaseItem().allowLay(); + + if (!preferred) { + continue; + } + + if (candidate == null || item.getZ() >= candidate.getZ()) { + candidate = item; + } + } + + if (candidate != null) { + return candidate; + } + + return room.getTopItemAt(tile.x, tile.y); + } + + public static double resolveUserTargetZ(Room room, RoomTile targetTile) { + if (room == null || targetTile == null || room.getLayout() == null) { + return 0; + } + + HabboItem targetItem = resolveEnteredItem(room, targetTile); + double targetZ = room.getLayout().getHeightAtSquare(targetTile.x, targetTile.y); + + if (targetItem != null) { + targetZ = targetItem.getZ(); + + if (!targetItem.getBaseItem().allowSit() && !targetItem.getBaseItem().allowLay()) { + targetZ += Item.getCurrentHeight(targetItem); + } + } + + for (HabboItem item : room.getItemsAt(targetTile)) { + if (item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { + targetZ = item.getZ(); + break; + } + } + + return targetZ; + } + + private static boolean occupiesTile(HabboItem item, RoomTile tile) { + if (item == null || tile == null || item.getBaseItem() == null) { + return false; + } + + return RoomLayout.pointInSquare( + item.getX(), + item.getY(), + item.getX() + item.getBaseItem().getWidth() - 1, + item.getY() + item.getBaseItem().getLength() - 1, + tile.x, + tile.y); + } + + private static List resolveAdditionalTileItems(Room room, RoomTile tile, HabboItem primaryItem) { + if (room == null || tile == null) { + return Collections.emptyList(); + } + + List items = new ArrayList<>(); + + for (HabboItem item : room.getItemsAt(tile)) { + if (item == null || item == primaryItem || !occupiesTile(item, tile)) { + continue; + } + + items.add(item); + } + + items.sort(Comparator + .comparingDouble((HabboItem item) -> item.getZ() + Item.getCurrentHeight(item)) + .thenComparingInt(HabboItem::getId) + .reversed()); + return items; } private static void schedulePostureSync(Room room, RoomUnit roomUnit, RoomTile targetTile, int delay) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java index 26f499c6..dba30ec8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java @@ -51,6 +51,10 @@ public class RoomUserWalkEvent extends MessageHandler { try { if (roomUnit != null && roomUnit.isInRoom() && roomUnit.canWalk() && !WiredFreezeUtil.isFrozen(roomUnit)) { + if (WiredUserMovementHelper.consumeSuppressedWalkCommand(roomUnit)) { + return; + } + if (roomUnit.cmdTeleport) { handleTeleport(room, (short) x, (short) y, roomUnit, habboInfo); return; From 50334d50e724afcc04c0f60e8608dd1e1c6e4584 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 7 Apr 2026 14:40:51 +0200 Subject: [PATCH 5/8] feat: add builders club catalog and room flow --- Database Updates/000_all_database_updates.sql | 104 ++++ .../009_add_builders_club_catalog_offers.sql | 5 + .../010_add_catalog_mode_to_catalog_pages.sql | 47 ++ .../011_add_builders_club_trial_room_lock.sql | 12 + ...2_support_builders_club_catalog_tables.sql | 44 ++ .../013_seed_builders_club_sample_page.sql | 171 ++++++ Default Database/FullDB.sql | 54 +- .../achievements/AchievementManager.java | 6 +- .../habbohotel/catalog/CatalogManager.java | 188 +++++- .../habbo/habbohotel/catalog/CatalogPage.java | 10 + .../habbohotel/catalog/CatalogPageType.java | 29 +- .../habbo/habbohotel/catalog/ClubOffer.java | 98 ++- .../commands/RoomBundleCommand.java | 3 +- .../rooms/BuildersClubRoomSupport.java | 574 ++++++++++++++++++ .../com/eu/habbo/habbohotel/rooms/Room.java | 80 ++- .../habbohotel/rooms/RoomItemManager.java | 83 ++- .../habbo/habbohotel/rooms/RoomManager.java | 14 + .../habbohotel/rooms/RoomRightsManager.java | 13 +- .../eu/habbo/habbohotel/users/HabboStats.java | 23 +- .../users/inventory/ItemsComponent.java | 2 +- .../users/subscriptions/Subscription.java | 1 + .../SubscriptionBuildersClub.java | 50 ++ .../subscriptions/SubscriptionManager.java | 1 + .../com/eu/habbo/messages/PacketManager.java | 4 +- .../eu/habbo/messages/incoming/Incoming.java | 3 + .../BuildersClubPlaceRoomItemEvent.java | 158 +++++ .../BuildersClubPlaceWallItemEvent.java | 140 +++++ .../BuildersClubQueryFurniCountEvent.java | 11 + .../incoming/catalog/CatalogBuyItemEvent.java | 48 +- .../catalog/RequestCatalogModeEvent.java | 13 +- .../catalog/RequestCatalogPageEvent.java | 4 +- .../CatalogAdminCreateOfferEvent.java | 32 +- .../CatalogAdminCreatePageEvent.java | 5 +- .../CatalogAdminDeleteOfferEvent.java | 4 +- .../CatalogAdminDeletePageEvent.java | 12 +- .../CatalogAdminMoveOfferEvent.java | 4 +- .../CatalogAdminMovePageEvent.java | 9 +- .../CatalogAdminSaveOfferEvent.java | 35 +- .../CatalogAdminSavePageEvent.java | 52 +- .../incoming/guilds/GuildDeleteEvent.java | 13 +- .../incoming/guilds/RequestGuildBuyEvent.java | 20 +- .../incoming/handshake/SecureLoginEvent.java | 33 +- .../rooms/items/RoomPlaceItemEvent.java | 27 + .../incoming/users/UserSaveLookEvent.java | 5 + .../eu/habbo/messages/outgoing/Outgoing.java | 1 + .../BuildersClubFurniCountComposer.java | 20 + ...uildersClubSubscriptionStatusComposer.java | 48 ++ .../catalog/CatalogPagesListComposer.java | 12 +- .../outgoing/catalog/ClubDataComposer.java | 10 +- .../generic/alerts/SimpleAlertComposer.java | 31 + docs/builders_club_catalog_reference.md | 284 +++++++++ 51 files changed, 2494 insertions(+), 156 deletions(-) create mode 100644 Database Updates/009_add_builders_club_catalog_offers.sql create mode 100644 Database Updates/010_add_catalog_mode_to_catalog_pages.sql create mode 100644 Database Updates/011_add_builders_club_trial_room_lock.sql create mode 100644 Database Updates/012_support_builders_club_catalog_tables.sql create mode 100644 Database Updates/013_seed_builders_club_sample_page.sql create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionBuildersClub.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubQueryFurniCountEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubFurniCountComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubSubscriptionStatusComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/generic/alerts/SimpleAlertComposer.java create mode 100644 docs/builders_club_catalog_reference.md diff --git a/Database Updates/000_all_database_updates.sql b/Database Updates/000_all_database_updates.sql index 53ee1db2..38f3dc93 100644 --- a/Database Updates/000_all_database_updates.sql +++ b/Database Updates/000_all_database_updates.sql @@ -29,6 +29,7 @@ -- 15. 17032026_allow_underpass.sql -- 16. 19032026_hotel_timezone.sql -- 17. 21022026_user_prefixes.sql +-- 18. 06042026_builders_club_catalog_offers.sql -- ============================================================================= SET NAMES utf8mb4; @@ -408,6 +409,109 @@ CREATE TABLE IF NOT EXISTS `user_prefixes` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- ============================================================================= +-- From: 06042026_builders_club_catalog_offers.sql +-- ============================================================================= +ALTER TABLE `catalog_club_offers` + MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC'; + +ALTER TABLE `catalog_pages` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `catalog_pages` + ADD COLUMN `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`; + +ALTER TABLE `catalog_pages_bc` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `users_settings` + ADD COLUMN IF NOT EXISTS `builders_club_bonus_furni` INT(11) NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`; + + -- ============================================================================= -- Done -- ============================================================================= diff --git a/Database Updates/009_add_builders_club_catalog_offers.sql b/Database Updates/009_add_builders_club_catalog_offers.sql new file mode 100644 index 00000000..cb4cca70 --- /dev/null +++ b/Database Updates/009_add_builders_club_catalog_offers.sql @@ -0,0 +1,5 @@ +ALTER TABLE `catalog_club_offers` + MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC'; + +ALTER TABLE `users_settings` + ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`; diff --git a/Database Updates/010_add_catalog_mode_to_catalog_pages.sql b/Database Updates/010_add_catalog_mode_to_catalog_pages.sql new file mode 100644 index 00000000..115eb78e --- /dev/null +++ b/Database Updates/010_add_catalog_mode_to_catalog_pages.sql @@ -0,0 +1,47 @@ +ALTER TABLE `catalog_pages` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `catalog_pages` + ADD COLUMN `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`; diff --git a/Database Updates/011_add_builders_club_trial_room_lock.sql b/Database Updates/011_add_builders_club_trial_room_lock.sql new file mode 100644 index 00000000..954d1f1b --- /dev/null +++ b/Database Updates/011_add_builders_club_trial_room_lock.sql @@ -0,0 +1,12 @@ +ALTER TABLE `rooms` + ADD COLUMN `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`, + ADD COLUMN `builders_club_original_state` VARCHAR(16) NOT NULL DEFAULT 'open' AFTER `builders_club_trial_locked`; + +CREATE TABLE IF NOT EXISTS `builders_club_items` ( + `item_id` INT(11) NOT NULL, + `user_id` INT(11) NOT NULL, + `room_id` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`item_id`), + KEY `idx_builders_club_items_user_id` (`user_id`), + KEY `idx_builders_club_items_room_id` (`room_id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; diff --git a/Database Updates/012_support_builders_club_catalog_tables.sql b/Database Updates/012_support_builders_club_catalog_tables.sql new file mode 100644 index 00000000..4e2f3181 --- /dev/null +++ b/Database Updates/012_support_builders_club_catalog_tables.sql @@ -0,0 +1,44 @@ +ALTER TABLE `catalog_pages_bc` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; diff --git a/Database Updates/013_seed_builders_club_sample_page.sql b/Database Updates/013_seed_builders_club_sample_page.sql new file mode 100644 index 00000000..d29745d2 --- /dev/null +++ b/Database Updates/013_seed_builders_club_sample_page.sql @@ -0,0 +1,171 @@ +-- Sample seed for Builders Club catalog pages/items +-- Safe to run multiple times: it recreates the demo BC pages and their demo BC offers. +-- After import, publish/reload the catalog. + +SET @bc_demo_root_caption := 'BC Demo Root'; +SET @bc_demo_page_caption := 'BC Demo Furni'; + +DELETE FROM `catalog_items_bc` +WHERE `page_id` IN ( + SELECT `id` + FROM ( + SELECT `id` + FROM `catalog_pages_bc` + WHERE `caption` IN (@bc_demo_root_caption, @bc_demo_page_caption) + ) AS `bc_pages_to_clear` +); + +DELETE FROM `catalog_pages_bc` +WHERE `caption` IN (@bc_demo_root_caption, @bc_demo_page_caption); + +INSERT INTO `catalog_pages_bc` +( + `parent_id`, + `caption`, + `page_layout`, + `icon_color`, + `icon_image`, + `order_num`, + `visible`, + `enabled`, + `page_headline`, + `page_teaser`, + `page_special`, + `page_text1`, + `page_text2`, + `page_text_details`, + `page_text_teaser` +) +VALUES +( + -1, + @bc_demo_root_caption, + 'default_3x3', + 1, + 28, + 999, + '1', + '1', + '', + '', + '', + 'Builders Club Demo', + 'Pagina demo creata da seed SQL', + 'Root demo per il catalogo Builders Club.', + '' +); + +SET @bc_demo_root_id := LAST_INSERT_ID(); + +INSERT INTO `catalog_pages_bc` +( + `parent_id`, + `caption`, + `page_layout`, + `icon_color`, + `icon_image`, + `order_num`, + `visible`, + `enabled`, + `page_headline`, + `page_teaser`, + `page_special`, + `page_text1`, + `page_text2`, + `page_text_details`, + `page_text_teaser` +) +VALUES +( + @bc_demo_root_id, + @bc_demo_page_caption, + 'default_3x3', + 1, + 28, + 1, + '1', + '1', + '', + '', + '', + 'Builders Club Furni', + 'Furni demo', + 'Questa pagina duplica alcuni furni del catalogo normale dentro al Builders Club.', + '' +); + +SET @bc_demo_page_id := LAST_INSERT_ID(); + +-- Source page from normal catalog: tries to use the "base" furni line. +SET @source_normal_page_id := ( + SELECT `id` + FROM `catalog_pages` + WHERE `caption_save` = 'base' + ORDER BY `id` + LIMIT 1 +); + +-- Copy only safe single-placeable demo furni: +-- - one single numeric item id +-- - floor furni only +-- - size 1x1 +-- - default interaction only +-- This avoids copying bundles, bots, effects, teleports, wall items and large furni that can fail placement during BC testing. +INSERT INTO `catalog_items_bc` +( + `item_ids`, + `page_id`, + `catalog_name`, + `order_number`, + `extradata` +) +SELECT + `ci`.`item_ids`, + @bc_demo_page_id, + CONCAT(`ci`.`catalog_name`, '_bc_demo'), + `ci`.`order_number`, + `ci`.`extradata` +FROM `catalog_items` `ci` +INNER JOIN `items_base` `ib` + ON `ib`.`id` = CAST(`ci`.`item_ids` AS UNSIGNED) +WHERE `ci`.`page_id` = @source_normal_page_id + AND `ci`.`item_ids` REGEXP '^[0-9]+$' + AND `ib`.`type` = 's' + AND `ib`.`width` = 1 + AND `ib`.`length` = 1 + AND `ib`.`interaction_type` = 'default' +ORDER BY `ci`.`order_number`, `ci`.`id` +LIMIT 6; + +-- Fallback: if page "base" is missing or empty, duplicate any 6 safe 1x1 floor offers. +INSERT INTO `catalog_items_bc` +( + `item_ids`, + `page_id`, + `catalog_name`, + `order_number`, + `extradata` +) +SELECT + `fallback_ci`.`item_ids`, + @bc_demo_page_id, + CONCAT(`fallback_ci`.`catalog_name`, '_bc_demo'), + `fallback_ci`.`id`, + `fallback_ci`.`extradata` +FROM `catalog_items` `fallback_ci` +INNER JOIN `items_base` `fallback_ib` + ON `fallback_ib`.`id` = CAST(`fallback_ci`.`item_ids` AS UNSIGNED) +WHERE NOT EXISTS ( + SELECT 1 + FROM `catalog_items_bc` + WHERE `page_id` = @bc_demo_page_id +) + AND `fallback_ci`.`item_ids` REGEXP '^[0-9]+$' + AND `fallback_ib`.`type` = 's' + AND `fallback_ib`.`width` = 1 + AND `fallback_ib`.`length` = 1 + AND `fallback_ib`.`interaction_type` = 'default' +ORDER BY `fallback_ci`.`id` +LIMIT 6; + +SELECT @bc_demo_root_id AS `bc_root_page_id`, @bc_demo_page_id AS `bc_furni_page_id`; diff --git a/Default Database/FullDB.sql b/Default Database/FullDB.sql index 80fc2a10..81660209 100644 --- a/Default Database/FullDB.sql +++ b/Default Database/FullDB.sql @@ -1530,7 +1530,7 @@ CREATE TABLE `catalog_club_offers` ( `credits` int(0) NOT NULL DEFAULT 10, `points` int(0) NOT NULL DEFAULT 0, `points_type` int(0) NOT NULL DEFAULT 0, - `type` enum('HC','VIP') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'HC', + `type` enum('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'HC', `deal` enum('0','1') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '0', `giftable` enum('1','0') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '0', PRIMARY KEY (`id`) USING BTREE @@ -13499,7 +13499,7 @@ CREATE TABLE `catalog_pages_bc` ( `id` int(0) NOT NULL AUTO_INCREMENT, `parent_id` int(0) NOT NULL DEFAULT -1, `caption` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, - `page_layout` enum('default_3x3','club_buy','club_gift','frontpage','spaces','recycler','recycler_info','recycler_prizes','trophies','plasto','marketplace','marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni','info_duckets','info_rentables','info_pets','roomads','single_bundle','sold_ltd_items','badge_display','bots','pets','pets2','pets3','productpage1','room_bundle','recent_purchases','default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty','loyalty_vip_buy','collectibles','petcustomization','frontpage_featured') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'default_3x3', + `page_layout` enum('default_3x3','club_buy','club_gift','frontpage','spaces','recycler','recycler_info','recycler_prizes','trophies','plasto','marketplace','marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni','info_duckets','info_rentables','info_pets','roomads','single_bundle','sold_ltd_items','badge_display','bots','pets','pets2','pets3','productpage1','room_bundle','recent_purchases','default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty','loyalty_vip_buy','collectibles','petcustomization','frontpage_featured','builders_club_frontpage','builders_club_addons','builders_club_loyalty') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'default_3x3', `icon_color` int(0) NOT NULL DEFAULT 1, `icon_image` int(0) NOT NULL DEFAULT 1, `order_num` int(0) NOT NULL DEFAULT 1, @@ -30209,6 +30209,7 @@ CREATE TABLE `users_settings` ( `ui_flags` int(0) NOT NULL DEFAULT 1, `has_gotten_default_saved_searches` tinyint(1) NOT NULL DEFAULT 0, `hc_gifts_claimed` int(0) NULL DEFAULT 0, + `builders_club_bonus_furni` int(0) NOT NULL DEFAULT 0, `last_hc_payday` int(0) NULL DEFAULT 0, `max_rooms` int(0) NULL DEFAULT 50, `max_friends` int(0) NULL DEFAULT 300, @@ -30223,7 +30224,7 @@ CREATE TABLE `users_settings` ( -- ---------------------------- -- Records of users_settings -- ---------------------------- -INSERT INTO `users_settings` VALUES (1, 1, 0, 0, 3, 3, 0, 0, 0, '0', '1', '0', 0, 0, 0, 0, 0, 0, 0, '0', '0', '0', 100, 100, 100, '0', '0', 0, 0, 0, 'Arcturus Emulator;', 0, 0, 0, 0, 0, '0', -1, -1, '0', '0', '0', 0, '0', '0', 0, 1, 1, 0, 0, 50, 300); +INSERT INTO `users_settings` VALUES (1, 1, 0, 0, 3, 3, 0, 0, 0, '0', '1', '0', 0, 0, 0, 0, 0, 0, 0, '0', '0', '0', 100, 100, 100, '0', '0', 0, 0, 0, 'Arcturus Emulator;', 0, 0, 0, 0, 0, '0', -1, -1, '0', '0', '0', 0, '0', '0', 0, 1, 1, 0, 0, 0, 50, 300); -- ---------------------------- -- Table structure for users_subscriptions @@ -30449,3 +30450,50 @@ INSERT INTO `youtube_playlists` VALUES (6587, 'PL4YfV2mXS8WXOkxFly7YsGL8cKtqp873 INSERT INTO `youtube_playlists` VALUES (6587, 'PL80F08DAE1B614BA9', 0); SET FOREIGN_KEY_CHECKS = 1; +ALTER TABLE `catalog_pages` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `catalog_pages` + ADD COLUMN `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NORMAL' AFTER `club_only`; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java index ff3700c4..7e5ecdb8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java @@ -228,6 +228,10 @@ public class AchievementManager { } public static int getAchievementProgressForHabbo(int userId, Achievement achievement) { + if (achievement == null) { + return 0; + } + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1")) { statement.setInt(1, userId); statement.setString(2, achievement.name); @@ -393,4 +397,4 @@ public class AchievementManager { public TalentTrackLevel getTalentTrackLevel(TalentTrackType type, int level) { return this.talentTrackLevels.get(type).get(level); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 643b3f44..9f937c66 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -188,6 +188,7 @@ public class CatalogManager { public static int PURCHASE_COOLDOWN = 1; public static boolean SORT_USING_ORDERNUM = false; public final TIntObjectMap catalogPages; + public final TIntObjectMap buildersClubCatalogPages; public final TIntObjectMap catalogFeaturedPages; public final THashMap> prizes; public final THashMap giftWrappers; @@ -197,6 +198,7 @@ public class CatalogManager { public final THashMap targetOffers; public final THashMap clothing; public final TIntIntHashMap offerDefs; + public final TIntIntHashMap buildersClubOfferDefs; public final Item ecotronItem; public final THashMap limitedNumbers; private final List vouchers; @@ -204,6 +206,7 @@ public class CatalogManager { public CatalogManager() { long millis = System.currentTimeMillis(); this.catalogPages = TCollections.synchronizedMap(new TIntObjectHashMap<>()); + this.buildersClubCatalogPages = TCollections.synchronizedMap(new TIntObjectHashMap<>()); this.catalogFeaturedPages = new TIntObjectHashMap<>(); this.prizes = new THashMap<>(); this.giftWrappers = new THashMap<>(); @@ -213,6 +216,7 @@ public class CatalogManager { this.targetOffers = new THashMap<>(); this.clothing = new THashMap<>(); this.offerDefs = new TIntIntHashMap(); + this.buildersClubOfferDefs = new TIntIntHashMap(); this.vouchers = new ArrayList<>(); this.limitedNumbers = new THashMap<>(); @@ -229,8 +233,10 @@ public class CatalogManager { this.loadLimitedNumbers(); this.loadCatalogPages(); + this.loadBuildersClubCatalogPages(); this.loadCatalogFeaturedPages(); this.loadCatalogItems(); + this.loadBuildersClubCatalogItems(); this.loadClubOffers(); this.loadTargetOffers(); this.loadVouchers(); @@ -315,6 +321,57 @@ public class CatalogManager { LOGGER.info("Loaded {} Catalog Pages!", this.catalogPages.size()); } + private synchronized void loadBuildersClubCatalogPages() { + this.buildersClubCatalogPages.clear(); + + final THashMap pages = new THashMap<>(); + pages.put(-1, new CatalogRootLayout()); + + String query = "SELECT id, parent_id, caption, caption AS caption_save, page_layout, icon_color, icon_image, 1 AS min_rank, order_num, visible, enabled, '0' AS club_only, 'BUILDERS_CLUB' AS catalog_mode, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser, '' AS includes FROM catalog_pages_bc ORDER BY parent_id, id"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + Class pageClazz = pageDefinitions.get(set.getString("page_layout")); + + if (pageClazz == null) { + LOGGER.info("Unknown Builders Club Page Layout: {}", set.getString("page_layout")); + continue; + } + + try { + CatalogPage page = pageClazz.getConstructor(ResultSet.class).newInstance(set); + pages.put(page.getId(), page); + } catch (Exception e) { + LOGGER.error("Failed to load Builders Club layout: {}", set.getString("page_layout")); + } + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + pages.forEachValue((object) -> { + CatalogPage page = pages.get(object.parentId); + + if (page != null) { + if (page.id != object.id) { + page.addChildPage(object); + } + } else { + if (object.parentId != -2) { + LOGGER.info("Builders Club parent page not found for {} (ID: {}, parent_id: {})", object.getPageName(), object.id, object.parentId); + } + } + return true; + }); + + this.buildersClubCatalogPages.putAll(pages); + + LOGGER.info("Loaded {} Builders Club Catalog Pages!", this.buildersClubCatalogPages.size()); + } + private synchronized void loadCatalogFeaturedPages() { this.catalogFeaturedPages.clear(); @@ -391,6 +448,53 @@ public class CatalogManager { } } + private synchronized void loadBuildersClubCatalogItems() { + this.buildersClubOfferDefs.clear(); + + String query = "SELECT id, item_ids, page_id, catalog_name, 0 AS cost_credits, 0 AS cost_points, 0 AS points_type, 1 AS amount, 0 AS limited_stack, 0 AS limited_sells, extradata, '0' AS club_only, '1' AS have_offer, id AS offer_id, order_number FROM catalog_items_bc"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + Statement statement = connection.createStatement(); + ResultSet set = statement.executeQuery(query)) { + CatalogItem item; + + while (set.next()) { + if (set.getString("item_ids").equals("0")) { + continue; + } + + CatalogPage page = this.buildersClubCatalogPages.get(set.getInt("page_id")); + + if (page == null) { + continue; + } + + item = page.getCatalogItem(set.getInt("id")); + + if (item == null) { + item = new CatalogItem(set); + page.addItem(item); + page.addOfferId(item.getOfferId()); + this.buildersClubOfferDefs.put(item.getOfferId(), item.getId()); + } else { + item.update(set); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + for (CatalogPage page : this.buildersClubCatalogPages.valueCollection()) { + for (Integer id : page.getIncluded()) { + CatalogPage includedPage = this.buildersClubCatalogPages.get(id); + + if (includedPage != null) { + page.getCatalogItems().putAll(includedPage.getCatalogItems()); + } + } + } + } + private void loadClubOffers() { this.clubOffers.clear(); @@ -585,6 +689,10 @@ public class CatalogManager { return this.catalogPages.get(pageId); } + public CatalogPage getCatalogPage(int pageId, CatalogPageType pageType) { + return this.getCatalogPagesMap(pageType).get(pageId); + } + public CatalogPage getCatalogPage(String captionSafe) { return this.catalogPages.valueCollection().stream() .filter(p -> p != null && p.getPageName() != null && p.getPageName().equalsIgnoreCase(captionSafe)) @@ -603,9 +711,15 @@ public class CatalogManager { } public CatalogItem getCatalogItem(int id) { + return this.getCatalogItem(id, CatalogPageType.NORMAL); + } + + public CatalogItem getCatalogItem(int id, CatalogPageType pageType) { final CatalogItem[] item = {null}; - synchronized (this.catalogPages) { - this.catalogPages.forEachValue(new TObjectProcedure() { + final TIntObjectMap pagesMap = this.getCatalogPagesMap(pageType); + + synchronized (pagesMap) { + pagesMap.forEachValue(new TObjectProcedure() { @Override public boolean execute(CatalogPage object) { item[0] = object.getCatalogItem(id); @@ -620,17 +734,28 @@ public class CatalogManager { public List getCatalogPages(int parentId, final Habbo habbo) { - final List pages = new ArrayList<>(); + return this.getCatalogPages(parentId, habbo, CatalogPageType.NORMAL); + } - this.catalogPages.get(parentId).childPages.forEachValue(new TObjectProcedure() { + public List getCatalogPages(int parentId, final Habbo habbo, final CatalogPageType pageType) { + final List pages = new ArrayList<>(); + final TIntObjectMap pagesMap = this.getCatalogPagesMap(pageType); + CatalogPage parentPage = pagesMap.get(parentId); + + if (parentPage == null) { + return pages; + } + + parentPage.childPages.forEachValue(new TObjectProcedure() { @Override public boolean execute(CatalogPage object) { boolean isVisiblePage = object.visible; boolean hasRightRank = object.getRank() <= habbo.getHabboInfo().getRank().getId(); boolean clubRightsOkay = !object.isClubOnly() || habbo.getHabboInfo().getHabboStats().hasActiveClub(); + boolean pageTypeMatches = (pageType == CatalogPageType.BUILDER) || object.getCatalogPageType().matches(pageType); - if (isVisiblePage && hasRightRank && clubRightsOkay) { + if (isVisiblePage && hasRightRank && clubRightsOkay && pageTypeMatches) { pages.add(object); } return true; @@ -704,22 +829,42 @@ public class CatalogManager { } - public CatalogPage createCatalogPage(String caption, String captionSave, int roomId, int icon, CatalogPageLayouts layout, int minRank, int parentId) { + public CatalogPage createCatalogPage(String caption, String captionSave, int roomId, int icon, CatalogPageLayouts layout, int minRank, int parentId, CatalogPageType pageType, CatalogPageType catalogMode) { CatalogPage catalogPage = null; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO catalog_pages (parent_id, caption, caption_save, icon_image, visible, enabled, min_rank, page_layout, room_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { + boolean buildersClubPage = (pageType == CatalogPageType.BUILDER); + String insertQuery = buildersClubPage + ? "INSERT INTO catalog_pages_bc (parent_id, caption, page_layout, icon_color, icon_image, order_num, visible, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + : "INSERT INTO catalog_pages (parent_id, caption, caption_save, icon_image, visible, enabled, min_rank, page_layout, room_id, catalog_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + String selectQuery = buildersClubPage + ? "SELECT id, parent_id, caption, caption AS caption_save, page_layout, icon_color, icon_image, 1 AS min_rank, order_num, visible, enabled, '0' AS club_only, 'BUILDERS_CLUB' AS catalog_mode, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser, '' AS includes FROM catalog_pages_bc WHERE id = ?" + : "SELECT * FROM catalog_pages WHERE id = ?"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(insertQuery, Statement.RETURN_GENERATED_KEYS)) { statement.setInt(1, parentId); statement.setString(2, caption); - statement.setString(3, captionSave); - statement.setInt(4, icon); - statement.setString(5, "1"); - statement.setString(6, "1"); - statement.setInt(7, minRank); - statement.setString(8, layout.name()); - statement.setInt(9, roomId); + + if (buildersClubPage) { + statement.setString(3, layout.name()); + statement.setInt(4, 1); + statement.setInt(5, icon); + statement.setInt(6, 1); + statement.setString(7, "1"); + statement.setString(8, "1"); + } else { + statement.setString(3, captionSave); + statement.setInt(4, icon); + statement.setString(5, "1"); + statement.setString(6, "1"); + statement.setInt(7, minRank); + statement.setString(8, layout.name()); + statement.setInt(9, roomId); + statement.setString(10, catalogMode.name()); + } statement.execute(); try (ResultSet set = statement.getGeneratedKeys()) { if (set.next()) { - try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM catalog_pages WHERE id = ?")) { + try (PreparedStatement stmt = connection.prepareStatement(selectQuery)) { stmt.setInt(1, set.getInt(1)); try (ResultSet page = stmt.executeQuery()) { if (page.next()) { @@ -744,7 +889,7 @@ public class CatalogManager { } if (catalogPage != null) { - this.catalogPages.put(catalogPage.getId(), catalogPage); + this.getCatalogPagesMap(pageType).put(catalogPage.getId(), catalogPage); } return catalogPage; @@ -1144,14 +1289,23 @@ public class CatalogManager { } public List getClubOffers() { + return this.getClubOffers(ClubOffer.WINDOW_HABBO_CLUB); + } + + public TIntObjectMap getCatalogPagesMap(CatalogPageType pageType) { + return (pageType == CatalogPageType.BUILDER) ? this.buildersClubCatalogPages : this.catalogPages; + } + + public List getClubOffers(int windowId) { List offers = new ArrayList<>(); for (Map.Entry entry : this.clubOffers.entrySet()) { - if (!entry.getValue().isDeal()) { + if (!entry.getValue().isDeal() && entry.getValue().belongsToWindow(windowId)) { offers.add(entry.getValue()); } } + offers.sort(Comparator.comparingInt(ClubOffer::getId)); return offers; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java index 6d36c279..2db319e1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java @@ -32,6 +32,7 @@ public abstract class CatalogPage implements Comparable, ISerialize protected boolean visible; protected boolean enabled; protected boolean clubOnly; + protected CatalogPageType catalogPageType = CatalogPageType.NORMAL; protected String layout; protected String headerImage; protected String teaserImage; @@ -59,6 +60,11 @@ public abstract class CatalogPage implements Comparable, ISerialize this.visible = set.getBoolean("visible"); this.enabled = set.getBoolean("enabled"); this.clubOnly = set.getBoolean("club_only"); + try { + this.catalogPageType = CatalogPageType.fromString(set.getString("catalog_mode")); + } catch (SQLException ignored) { + this.catalogPageType = CatalogPageType.NORMAL; + } this.layout = set.getString("page_layout"); this.headerImage = set.getString("page_headline"); this.teaserImage = set.getString("page_teaser"); @@ -128,6 +134,10 @@ public abstract class CatalogPage implements Comparable, ISerialize return this.clubOnly; } + public CatalogPageType getCatalogPageType() { + return this.catalogPageType; + } + public String getLayout() { return this.layout; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java index d270395f..993ea34e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java @@ -4,6 +4,33 @@ public enum CatalogPageType { NORMAL, + BUILDER, - BUILDER + BOTH; + + public static CatalogPageType fromString(String value) { + if (value == null || value.isEmpty()) { + return NORMAL; + } + + switch (value.trim().toUpperCase()) { + case "BUILDERS_CLUB": + case "BUILDER": + case "BC": + return BUILDER; + case "BOTH": + return BOTH; + case "NORMAL": + default: + return NORMAL; + } + } + + public boolean matches(CatalogPageType requestedType) { + if (this == BOTH || requestedType == BOTH) { + return true; + } + + return this == requestedType; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java index b7654416..3f357713 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java @@ -10,6 +10,30 @@ import java.util.Calendar; import java.util.TimeZone; public class ClubOffer implements ISerialize { + public static final int WINDOW_HABBO_CLUB = 1; + public static final int WINDOW_BUILDERS_CLUB = 2; + public static final int WINDOW_BUILDERS_CLUB_ADDONS = 3; + + public enum OfferType { + HC, + VIP, + BUILDERS_CLUB, + BUILDERS_CLUB_ADDON; + + public static OfferType fromDatabase(String value) { + if (value == null) { + return HC; + } + + for (OfferType type : OfferType.values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } + } + + return HC; + } + } private final int id; @@ -28,12 +52,16 @@ public class ClubOffer implements ISerialize { private final int pointsType; + private final OfferType type; + private final boolean vip; private final boolean deal; + private final boolean giftable; + public ClubOffer(ResultSet set) throws SQLException { this.id = set.getInt("id"); this.name = set.getString("name"); @@ -41,8 +69,10 @@ public class ClubOffer implements ISerialize { this.credits = set.getInt("credits"); this.points = set.getInt("points"); this.pointsType = set.getInt("points_type"); - this.vip = set.getString("type").equalsIgnoreCase("vip"); + this.type = OfferType.fromDatabase(set.getString("type")); + this.vip = this.type == OfferType.VIP; this.deal = set.getString("deal").equals("1"); + this.giftable = set.getString("giftable").equals("1"); } public int getId() { @@ -69,6 +99,10 @@ public class ClubOffer implements ISerialize { return this.pointsType; } + public OfferType getType() { + return this.type; + } + public boolean isVip() { return this.vip; } @@ -77,13 +111,49 @@ public class ClubOffer implements ISerialize { return this.deal; } + public boolean isGiftable() { + return this.giftable; + } + + public boolean isBuildersClubSubscription() { + return this.type == OfferType.BUILDERS_CLUB; + } + + public boolean isBuildersClubAddon() { + return this.type == OfferType.BUILDERS_CLUB_ADDON; + } + + public boolean isHabboClubOffer() { + return this.type == OfferType.HC || this.type == OfferType.VIP; + } + + public boolean isSubscriptionOffer() { + return !this.isBuildersClubAddon(); + } + + public int getWindowId() { + if (this.isBuildersClubAddon()) { + return WINDOW_BUILDERS_CLUB_ADDONS; + } + + if (this.isBuildersClubSubscription()) { + return WINDOW_BUILDERS_CLUB; + } + + return WINDOW_HABBO_CLUB; + } + + public boolean belongsToWindow(int windowId) { + return this.getWindowId() == windowId; + } + @Override public void serialize(ServerMessage message) { serialize(message, Emulator.getIntUnixTimestamp()); } - public void serialize(ServerMessage message, int hcExpireTimestamp) { - hcExpireTimestamp = Math.max(Emulator.getIntUnixTimestamp(), hcExpireTimestamp); + public void serialize(ServerMessage message, int expireTimestamp) { + expireTimestamp = Math.max(Emulator.getIntUnixTimestamp(), expireTimestamp); message.appendInt(this.id); message.appendString(this.name); message.appendBoolean(false); //unused @@ -96,27 +166,29 @@ public class ClubOffer implements ISerialize { long secondsTotal = seconds; - int totalYears = (int) Math.floor((int) seconds / (86400.0 * 31 * 12)); + int totalYears = (int) Math.floor(seconds / (86400.0 * 31 * 12)); seconds -= totalYears * (86400 * 31 * 12); - int totalMonths = (int) Math.floor((int) seconds / (86400.0 * 31)); + int totalMonths = (int) Math.floor(seconds / (86400.0 * 31)); seconds -= totalMonths * (86400 * 31); - int totalDays = (int) Math.floor((int) seconds / 86400.0); + int totalDays = (int) Math.floor(seconds / 86400.0); seconds -= totalDays * 86400L; - message.appendInt((int) secondsTotal / 86400 / 31); - message.appendInt((int) seconds); - message.appendBoolean(false); //giftable - message.appendInt((int) seconds); + message.appendInt(totalMonths); + message.appendInt(totalDays); + message.appendBoolean(this.giftable); + message.appendInt(totalDays); - hcExpireTimestamp += secondsTotal; + if (this.isSubscriptionOffer()) { + expireTimestamp += secondsTotal; + } Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone("UTC")); - cal.setTimeInMillis(hcExpireTimestamp * 1000L); + cal.setTimeInMillis(expireTimestamp * 1000L); message.appendInt(cal.get(Calendar.YEAR)); message.appendInt(cal.get(Calendar.MONTH) + 1); message.appendInt(cal.get(Calendar.DAY_OF_MONTH)); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java index e1992e98..e2776daa 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogItem; import com.eu.habbo.habbohotel.catalog.CatalogPage; import com.eu.habbo.habbohotel.catalog.CatalogPageLayouts; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.catalog.layouts.RoomBundleLayout; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; @@ -41,7 +42,7 @@ public class RoomBundleCommand extends Command { points = Integer.parseInt(params[3]); pointsType = Integer.parseInt(params[4]); - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage("Room Bundle: " + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getName(), "room_bundle_" + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), 0, CatalogPageLayouts.room_bundle, gameClient.getHabbo().getHabboInfo().getRank().getId(), parentId); + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage("Room Bundle: " + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getName(), "room_bundle_" + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), 0, CatalogPageLayouts.room_bundle, gameClient.getHabbo().getHabboInfo().getRank().getId(), parentId, CatalogPageType.NORMAL, CatalogPageType.NORMAL); if (page instanceof RoomBundleLayout) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type ) VALUES (?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java new file mode 100644 index 00000000..9d2ac676 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java @@ -0,0 +1,574 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildMember; +import com.eu.habbo.habbohotel.guilds.GuildRank; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboInfo; +import com.eu.habbo.habbohotel.users.subscriptions.Subscription; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.generic.alerts.SimpleAlertComposer; +import gnu.trove.map.hash.THashMap; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +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. + public static final int VIRTUAL_OWNER_ID = 1; + public static final String DISPLAY_OWNER_NAME = "Builders Club"; + + public enum SyncResult { + UNCHANGED, + LOCKED, + UNLOCKED + } + + private BuildersClubRoomSupport() { + } + + public static int getFurniLimit(Habbo habbo) { + if (habbo == null) { + return DEFAULT_TRIAL_FURNI_LIMIT; + } + + return DEFAULT_TRIAL_FURNI_LIMIT + Math.max(0, habbo.getHabboStats().getBuildersClubBonusFurni()); + } + + public static int getFurniLimit(int userId) { + HabboInfo habboInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + if (habboInfo == null || habboInfo.getHabboStats() == null) { + return DEFAULT_TRIAL_FURNI_LIMIT; + } + + return DEFAULT_TRIAL_FURNI_LIMIT + Math.max(0, habboInfo.getHabboStats().getBuildersClubBonusFurni()); + } + + public static int getMembershipSecondsLeft(int userId) { + HabboInfo habboInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + if (habboInfo == null || habboInfo.getHabboStats() == null) { + return 0; + } + + Subscription subscription = habboInfo.getHabboStats().getSubscription(Subscription.BUILDERS_CLUB); + + if (subscription == null) { + return 0; + } + + return Math.max(0, subscription.getRemaining()); + } + + public static boolean hasActiveMembership(int userId) { + HabboInfo habboInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + return habboInfo != null + && habboInfo.getHabboStats() != null + && habboInfo.getHabboStats().hasSubscription(Subscription.BUILDERS_CLUB); + } + + public static int getTrackedFurniCount(int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM builders_club_items WHERE user_id = ? AND room_id > 0")) { + statement.setInt(1, userId); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return set.getInt(1); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception counting Builders Club furni", e); + } + + return 0; + } + + public static boolean hasTrackedItemsInOwnedRooms(int ownerId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM builders_club_items bci INNER JOIN rooms r ON r.id = bci.room_id WHERE r.owner_id = ? AND bci.room_id > 0 LIMIT 1")) { + statement.setInt(1, ownerId); + + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception checking Builders Club room ownership", e); + } + + return false; + } + + public static boolean roomHasTrackedItems(int roomId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM builders_club_items WHERE room_id = ? LIMIT 1")) { + statement.setInt(1, roomId); + + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception checking Builders Club room items", e); + } + + return false; + } + + public static boolean isTrackedItem(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM builders_club_items WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception checking Builders Club tracked item", e); + } + + return false; + } + + public static int getTrackedUserId(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT user_id FROM builders_club_items WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return set.getInt("user_id"); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception getting Builders Club tracked user", e); + } + + return 0; + } + + public static boolean hasPlacementVisitors(Room room, Habbo owner) { + if (room == null || owner == null) { + return false; + } + + for (Habbo habbo : room.getHabbos()) { + if (habbo == null || habbo.getHabboInfo() == null) { + continue; + } + + if (habbo.getHabboInfo().getId() == owner.getHabboInfo().getId()) { + continue; + } + + if (habbo.hasPermission(Permission.ACC_ENTERANYROOM) || habbo.hasPermission(Permission.ACC_ANYROOMOWNER)) { + continue; + } + + return true; + } + + return false; + } + + public static boolean isPlacementBlockedByVisitors(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return false; + } + + if (hasActiveMembership(habbo.getHabboInfo().getId())) { + return false; + } + + Room currentRoom = habbo.getHabboInfo().getCurrentRoom(); + + if (currentRoom == null || currentRoom.getOwnerId() != habbo.getHabboInfo().getId()) { + return false; + } + + return hasPlacementVisitors(currentRoom, habbo); + } + + public static boolean canPlaceInCurrentRoom(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getCurrentRoom() == null) { + return false; + } + + return canPlaceInRoom(habbo, habbo.getHabboInfo().getCurrentRoom()); + } + + public static boolean canPlaceInRoom(Habbo habbo, Room room) { + if (habbo == null || habbo.getHabboInfo() == null || room == null) { + return false; + } + + if (room.getOwnerId() == habbo.getHabboInfo().getId()) { + return true; + } + + Room currentRoom = habbo.getHabboInfo().getCurrentRoom(); + + if (currentRoom == null || currentRoom.getId() != room.getId()) { + return false; + } + + return canUseGuildPlacementPool(habbo, room); + } + + public static int getPlacementPoolUserId(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return 0; + } + + Room currentRoom = habbo.getHabboInfo().getCurrentRoom(); + + if (currentRoom == null) { + return habbo.getHabboInfo().getId(); + } + + if (currentRoom.getOwnerId() == habbo.getHabboInfo().getId()) { + return habbo.getHabboInfo().getId(); + } + + if (canUseGuildPlacementPool(habbo, currentRoom)) { + Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(currentRoom.getGuildId()); + + if (guild != null && guild.getOwnerId() > 0) { + return guild.getOwnerId(); + } + } + + return habbo.getHabboInfo().getId(); + } + + public static int getPlacementPoolFurniCount(Habbo habbo) { + int userId = getPlacementPoolUserId(habbo); + + if (userId <= 0) { + return 0; + } + + return getTrackedFurniCount(userId); + } + + public static int getPlacementPoolFurniLimit(Habbo habbo) { + int userId = getPlacementPoolUserId(habbo); + + if (userId <= 0) { + return DEFAULT_TRIAL_FURNI_LIMIT; + } + + return getFurniLimit(userId); + } + + public static void sendPlacementStatus(Habbo habbo) { + if (habbo == null || habbo.getClient() == null) { + return; + } + + habbo.getClient().sendResponse(new BuildersClubFurniCountComposer(getTrackedFurniCount(habbo.getHabboInfo().getId()))); + habbo.getClient().sendResponse(new BuildersClubSubscriptionStatusComposer(habbo)); + } + + public static void sendPlacementStatusForPool(Room room, int placementUserId) { + if (placementUserId <= 0) { + return; + } + + THashSet updatedUsers = new THashSet<>(); + + if (room != null) { + for (Habbo habbo : room.getHabbos()) { + if (habbo == null || habbo.getHabboInfo() == null) { + continue; + } + + if (getPlacementPoolUserId(habbo) != placementUserId) { + continue; + } + + sendPlacementStatus(habbo); + updatedUsers.add(habbo.getHabboInfo().getId()); + } + } + + Habbo placementPoolHabbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(placementUserId); + + if (placementPoolHabbo != null && placementPoolHabbo.getHabboInfo() != null && !updatedUsers.contains(placementPoolHabbo.getHabboInfo().getId())) { + sendPlacementStatus(placementPoolHabbo); + } + } + + public static void sendCurrentRoomPlacementStatus(Room room) { + if (room == null) { + return; + } + + Habbo owner = room.getHabbo(room.getOwnerId()); + + if (owner == null || owner.getClient() == null) { + return; + } + + owner.getClient().sendResponse(new com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer(owner)); + } + + private static boolean canUseGuildPlacementPool(Habbo habbo, Room room) { + if (habbo == null || room == null) { + return false; + } + + Guild guild = resolvePlacementGuild(room); + + if (guild == null || guild.getOwnerId() <= 0) { + return false; + } + + boolean isGuildAdmin = room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_ADMIN); + + if (!isGuildAdmin) { + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild.getId(), habbo.getHabboInfo().getId()); + + isGuildAdmin = member != null && (member.getRank() == GuildRank.ADMIN || member.getRank() == GuildRank.OWNER); + } + + if (!isGuildAdmin) { + return false; + } + + return hasActiveMembership(habbo.getHabboInfo().getId()) && hasActiveMembership(guild.getOwnerId()); + } + + private static Guild resolvePlacementGuild(Room room) { + int guildId = resolveRoomGuildId(room); + + if (guildId <= 0) { + return null; + } + + if (room.getGuildId() != guildId) { + room.setGuild(guildId); + } + + return Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); + } + + private static int resolveRoomGuildId(Room room) { + if (room == null) { + return 0; + } + + if (room.getGuildId() > 0) { + return room.getGuildId(); + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT guild_id FROM rooms WHERE id = ? LIMIT 1")) { + statement.setInt(1, room.getId()); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return set.getInt("guild_id"); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception resolving Builders Club room guild", e); + } + + return 0; + } + + public static void trackPlacedItem(int itemId, int userId, int roomId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO builders_club_items (item_id, user_id, room_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), room_id = VALUES(room_id)")) { + statement.setInt(1, itemId); + statement.setInt(2, userId); + statement.setInt(3, roomId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception tracking Builders Club item placement", e); + } + } + + public static void clearTrackedItemRoom(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE builders_club_items SET room_id = 0 WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception clearing Builders Club room assignment", e); + } + } + + public static void deleteTrackedItem(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM builders_club_items WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception deleting Builders Club tracked item", e); + } + } + + public static SyncResult syncRoom(Room room) { + if (room == null) { + return SyncResult.UNCHANGED; + } + + boolean hasTrackedItems = roomHasTrackedItems(room.getId()); + boolean hasMembership = hasActiveMembership(room.getOwnerId()); + + if (hasTrackedItems && !hasMembership) { + return lockRoom(room) ? SyncResult.LOCKED : SyncResult.UNCHANGED; + } + + if (room.isBuildersClubTrialLocked() && (!hasTrackedItems || hasMembership)) { + return unlockRoom(room) ? SyncResult.UNLOCKED : SyncResult.UNCHANGED; + } + + return SyncResult.UNCHANGED; + } + + public static int syncOwnedRooms(int ownerId) { + int changed = 0; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT id FROM rooms WHERE owner_id = ?")) { + statement.setInt(1, ownerId); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(set.getInt("id"), false); + + if (syncRoom(room) != SyncResult.UNCHANGED) { + changed++; + } + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception syncing Builders Club rooms", e); + } + + return changed; + } + + public static void sendRoomLockedBubble(int ownerId) { + sendBubbleNotification(ownerId, BubbleAlertKeys.BUILDERS_CLUB_ROOM_LOCKED, null); + } + + public static void sendRoomUnlockedBubble(int ownerId) { + sendBubbleNotification(ownerId, BubbleAlertKeys.BUILDERS_CLUB_ROOM_UNLOCKED, null); + } + + public static void sendMembershipMadeBubble(int userId) { + sendBubbleNotification(userId, BubbleAlertKeys.BUILDERS_CLUB_MEMBERSHIP_MADE, null); + } + + public static void sendMembershipExtendedBubble(int userId) { + sendBubbleNotification(userId, BubbleAlertKeys.BUILDERS_CLUB_MEMBERSHIP_EXTENDED, null); + } + + public static void sendVisitDeniedOwnerBubble(int ownerId, String username) { + THashMap keys = new THashMap<>(); + keys.put("USERNAME", username); + + sendBubbleNotification(ownerId, BubbleAlertKeys.BUILDERS_CLUB_VISIT_DENIED_OWNER, keys); + } + + public static void sendVisitDeniedVisitorAlert(int userId) { + sendSimpleAlert(userId, "notification.builders_club.visit_denied_for_visitor.message"); + } + + public static void sendMembershipExpiringAlert(int userId) { + sendSimpleAlert(userId, "expiring.bc.membership.description"); + } + + public static void sendMembershipExpiredAlert(int userId, boolean hasTrackedRooms) { + sendSimpleAlert( + userId, + hasTrackedRooms + ? "notification.builders_club.membership_expired.message" + : "notification.builders_club.membership_expired.message_no_rooms" + ); + } + + private static void sendBubbleNotification(int userId, BubbleAlertKeys key, THashMap keys) { + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (habbo == null || habbo.getClient() == null) { + return; + } + + if (keys == null) { + habbo.getClient().sendResponse(new BubbleAlertComposer(key.key)); + return; + } + + habbo.getClient().sendResponse(new BubbleAlertComposer(key.key, keys)); + } + + private static void sendSimpleAlert(int userId, String messageKey) { + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (habbo == null || habbo.getClient() == null) { + return; + } + + habbo.getClient().sendResponse(new SimpleAlertComposer(messageKey)); + } + + private static boolean lockRoom(Room room) { + if (room.isBuildersClubTrialLocked()) { + if (room.getState() != RoomState.INVISIBLE) { + room.setState(RoomState.INVISIBLE); + room.setNeedsUpdate(true); + room.save(); + } + + return false; + } + + room.setBuildersClubOriginalState(room.getState()); + room.setBuildersClubTrialLocked(true); + room.setState(RoomState.INVISIBLE); + room.setNeedsUpdate(true); + room.save(); + + return true; + } + + private static boolean unlockRoom(Room room) { + if (!room.isBuildersClubTrialLocked()) { + return false; + } + + RoomState originalState = room.getBuildersClubOriginalState(); + + if (originalState == null) { + originalState = RoomState.OPEN; + } + + room.setState(originalState); + room.setBuildersClubTrialLocked(false); + room.setBuildersClubOriginalState(originalState); + room.setNeedsUpdate(true); + room.save(); + + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index 87096178..77ee4857 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -177,6 +177,8 @@ public class Room implements Comparable, ISerialize, Runnable { private boolean allowUnderpass; private boolean jukeboxActive; private boolean hideWired; + private boolean buildersClubTrialLocked; + private RoomState buildersClubOriginalState; private RoomPromotion promotion; private volatile boolean needsUpdate; private volatile boolean loaded; @@ -236,6 +238,19 @@ public class Room implements Comparable, ISerialize, Runnable { this.promoted = set.getString("promoted").equals("1"); this.jukeboxActive = set.getString("jukebox_active").equals("1"); this.hideWired = set.getString("hidewired").equals("1"); + this.buildersClubTrialLocked = set.getBoolean("builders_club_trial_locked"); + + String buildersClubOriginalState = set.getString("builders_club_original_state"); + + if (buildersClubOriginalState != null && !buildersClubOriginalState.isEmpty()) { + try { + this.buildersClubOriginalState = RoomState.valueOf(buildersClubOriginalState.toUpperCase()); + } catch (IllegalArgumentException e) { + this.buildersClubOriginalState = RoomState.OPEN; + } + } else { + this.buildersClubOriginalState = RoomState.OPEN; + } this.bannedHabbos = new TIntObjectHashMap<>(); @@ -781,6 +796,8 @@ public class Room implements Comparable, ISerialize, Runnable { return; } + boolean trackedBuildersClubItem = BuildersClubRoomSupport.isTrackedItem(item.getId()); + if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) { Event furniturePickedUpEvent = new FurniturePickedUpEvent(item, picker); Emulator.getPluginManager().fireEvent(furniturePickedUpEvent); @@ -823,9 +840,14 @@ public class Room implements Comparable, ISerialize, Runnable { this.sendComposer(new RemoveWallItemComposer(item).compose()); } + if (trackedBuildersClubItem) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + return; + } + Habbo habbo = (picker != null && picker.getHabboInfo().getId() == item.getId() ? picker : Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId())); - if (habbo != null) { + if (!trackedBuildersClubItem && habbo != null) { habbo.getInventory().getItemsComponent().addItem(item); habbo.getClient().sendResponse(new AddHabboItemComposer(item)); habbo.getClient().sendResponse(new InventoryRefreshComposer()); @@ -1129,7 +1151,7 @@ public class Room implements Comparable, ISerialize, Runnable { if (this.needsUpdate) { try (Connection connection = Emulator.getDatabase().getDataSource() .getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ? WHERE id = ?")) { + "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) { statement.setString(1, this.name); statement.setString(2, this.description); statement.setString(3, this.password); @@ -1179,7 +1201,9 @@ public class Room implements Comparable, ISerialize, Runnable { statement.setString(38, this.jukeboxActive ? "1" : "0"); statement.setString(39, this.hideWired ? "1" : "0"); statement.setString(40, this.allowUnderpass ? "1" : "0"); - statement.setInt(41, this.id); + statement.setString(41, this.buildersClubTrialLocked ? "1" : "0"); + statement.setString(42, (this.buildersClubOriginalState != null ? this.buildersClubOriginalState : RoomState.OPEN).name().toLowerCase()); + statement.setInt(43, this.id); statement.executeUpdate(); this.needsUpdate = false; } catch (SQLException e) { @@ -1296,6 +1320,22 @@ public class Room implements Comparable, ISerialize, Runnable { this.state = state; } + public boolean isBuildersClubTrialLocked() { + return this.buildersClubTrialLocked; + } + + public void setBuildersClubTrialLocked(boolean buildersClubTrialLocked) { + this.buildersClubTrialLocked = buildersClubTrialLocked; + } + + public RoomState getBuildersClubOriginalState() { + return this.buildersClubOriginalState; + } + + public void setBuildersClubOriginalState(RoomState buildersClubOriginalState) { + this.buildersClubOriginalState = buildersClubOriginalState; + } + public int getUsersMax() { return this.usersMax; } @@ -1395,11 +1435,28 @@ public class Room implements Comparable, ISerialize, Runnable { } public int getGuildId() { + if (this.guild > 0) { + return this.guild; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT guild_id FROM rooms WHERE id = ? LIMIT 1")) { + statement.setInt(1, this.id); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + this.guild = set.getInt("guild_id"); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception resolving room guild", e); + } + return this.guild; } public boolean hasGuild() { - return this.guild != 0; + return this.getGuildId() != 0; } public void setGuild(int guild) { @@ -2129,11 +2186,18 @@ public class Room implements Comparable, ISerialize, Runnable { } public RoomRightLevels getGuildRightLevel(Habbo habbo) { - if (this.guild > 0 && habbo.getHabboStats().hasGuild(this.guild)) { - Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(this.guild); + int guildId = this.getGuildId(); - if (Emulator.getGameEnvironment().getGuildManager().getOnlyAdmins(guild) - .get(habbo.getHabboInfo().getId()) != null) { + if (guildId > 0 && habbo != null && habbo.getHabboInfo() != null) { + Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); + + if (guild == null) { + return RoomRightLevels.NONE; + } + + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild.getId(), habbo.getHabboInfo().getId()); + + if ((member != null) && (member.getRank() == GuildRank.ADMIN || member.getRank() == GuildRank.OWNER)) { return RoomRightLevels.GUILD_ADMIN; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index 9b36d828..e6f991f1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -627,19 +627,28 @@ public class RoomItemManager { } } + if (BuildersClubRoomSupport.isTrackedItem(item.getId()) && item.getUserId() != BuildersClubRoomSupport.VIRTUAL_OWNER_ID) { + item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); + item.needsUpdate(true); + } + synchronized (this.furniOwnerCount) { this.furniOwnerCount.put(item.getUserId(), this.furniOwnerCount.get(item.getUserId()) + 1); } synchronized (this.furniOwnerNames) { if (!this.furniOwnerNames.containsKey(item.getUserId())) { - HabboInfo habbo = HabboManager.getOfflineHabboInfo(item.getUserId()); - - if (habbo != null) { - this.furniOwnerNames.put(item.getUserId(), habbo.getUsername()); + if (item.getUserId() == BuildersClubRoomSupport.VIRTUAL_OWNER_ID && BuildersClubRoomSupport.isTrackedItem(item.getId())) { + this.furniOwnerNames.put(item.getUserId(), BuildersClubRoomSupport.DISPLAY_OWNER_NAME); } else { - LOGGER.error("Failed to find username for item (ID: {}, UserID: {})", - item.getId(), item.getUserId()); + HabboInfo habbo = HabboManager.getOfflineHabboInfo(item.getUserId()); + + if (habbo != null) { + this.furniOwnerNames.put(item.getUserId(), habbo.getUsername()); + } else { + LOGGER.error("Failed to find username for item (ID: {}, UserID: {})", + item.getId(), item.getUserId()); + } } } } @@ -749,6 +758,9 @@ public class RoomItemManager { return; } + boolean trackedBuildersClubItem = BuildersClubRoomSupport.isTrackedItem(item.getId()); + int trackedUserId = trackedBuildersClubItem ? BuildersClubRoomSupport.getTrackedUserId(item.getId()) : item.getUserId(); + HabboItem i; synchronized (this.roomItems) { i = this.roomItems.remove(item.getId()); @@ -771,6 +783,16 @@ public class RoomItemManager { // Unregister from special types this.unregisterItemFromSpecialTypes(item); } + + if (trackedBuildersClubItem) { + BuildersClubRoomSupport.deleteTrackedItem(item.getId()); + + if (BuildersClubRoomSupport.syncRoom(this.room) == BuildersClubRoomSupport.SyncResult.UNLOCKED) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(this.room.getOwnerId()); + } + + BuildersClubRoomSupport.sendPlacementStatusForPool(this.room, trackedUserId); + } } /** @@ -992,6 +1014,8 @@ public class RoomItemManager { return; } + boolean trackedBuildersClubItem = BuildersClubRoomSupport.isTrackedItem(item.getId()); + if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) { FurniturePickedUpEvent event = Emulator.getPluginManager() .fireEvent(new FurniturePickedUpEvent(item, picker)); @@ -1036,6 +1060,11 @@ public class RoomItemManager { this.room.sendComposer(new RemoveWallItemComposer(item).compose()); } + if (trackedBuildersClubItem) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + return; + } + Emulator.getThreading().run(item); } @@ -1044,6 +1073,7 @@ public class RoomItemManager { */ public void ejectUserFurni(int userId) { THashSet items = new THashSet<>(); + THashSet inventoryItems = new THashSet<>(); TIntObjectIterator iterator = this.roomItems.iterator(); @@ -1056,15 +1086,20 @@ public class RoomItemManager { if (iterator.value().getUserId() == userId) { items.add(iterator.value()); + + if (!BuildersClubRoomSupport.isTrackedItem(iterator.value().getId())) { + inventoryItems.add(iterator.value()); + } + iterator.value().setRoomId(0); } } Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); - if (habbo != null) { - habbo.getInventory().getItemsComponent().addItems(items); - habbo.getClient().sendResponse(new AddHabboItemComposer(items)); + if (habbo != null && !inventoryItems.isEmpty()) { + habbo.getInventory().getItemsComponent().addItems(inventoryItems); + habbo.getClient().sendResponse(new AddHabboItemComposer(inventoryItems)); } for (HabboItem i : items) { @@ -1116,15 +1151,23 @@ public class RoomItemManager { } for (Map.Entry> entrySet : userItemsMap.entrySet()) { + THashSet inventoryItems = new THashSet<>(); + + for (HabboItem item : entrySet.getValue()) { + if (!BuildersClubRoomSupport.isTrackedItem(item.getId())) { + inventoryItems.add(item); + } + } + for (HabboItem i : entrySet.getValue()) { this.pickUpItem(i, null); } Habbo user = Emulator.getGameEnvironment().getHabboManager().getHabbo(entrySet.getKey()); - if (user != null) { - user.getInventory().getItemsComponent().addItems(entrySet.getValue()); - user.getClient().sendResponse(new AddHabboItemComposer(entrySet.getValue())); + if (user != null && !inventoryItems.isEmpty()) { + user.getInventory().getItemsComponent().addItems(inventoryItems); + user.getClient().sendResponse(new AddHabboItemComposer(inventoryItems)); } } } @@ -1268,7 +1311,7 @@ public class RoomItemManager { rotation %= 8; if (this.room.hasRights(habbo) || this.room.getGuildRightLevel(habbo) .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission( - Permission.ACC_MOVEROTATE)) { + Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) { return FurnitureMovementError.NONE; } @@ -1526,7 +1569,7 @@ public class RoomItemManager { item.setY(tile.y); item.setRotation(rotation); if (!this.furniOwnerNames.containsKey(item.getUserId()) && owner != null) { - this.furniOwnerNames.put(item.getUserId(), owner.getHabboInfo().getUsername()); + this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner)); } item.needsUpdate(true); @@ -1563,7 +1606,7 @@ public class RoomItemManager { */ public FurnitureMovementError placeWallFurniAt(HabboItem item, String wallPosition, Habbo owner) { if (!(this.room.hasRights(owner) || this.room.getGuildRightLevel(owner) - .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS))) { + .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) { return FurnitureMovementError.NO_RIGHTS; } @@ -1578,7 +1621,7 @@ public class RoomItemManager { item.setWallPosition(wallPosition); if (!this.furniOwnerNames.containsKey(item.getUserId()) && owner != null) { - this.furniOwnerNames.put(item.getUserId(), owner.getHabboInfo().getUsername()); + this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner)); } this.room.sendComposer( new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); @@ -1590,6 +1633,14 @@ public class RoomItemManager { return FurnitureMovementError.NONE; } + private String resolveOwnerName(HabboItem item, Habbo owner) { + if (item != null && item.getUserId() == BuildersClubRoomSupport.VIRTUAL_OWNER_ID) { + return BuildersClubRoomSupport.DISPLAY_OWNER_NAME; + } + + return (owner != null) ? owner.getHabboInfo().getUsername() : ""; + } + /** * Moves furniture to a new position with an explicit Z height. */ diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java index fd25c1e7..a22414e9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java @@ -569,6 +569,18 @@ public class RoomManager { return; } + if (room.isBuildersClubTrialLocked() + && habbo.getHabboInfo().getId() != room.getOwnerId() + && !overrideChecks + && !habbo.hasPermission(Permission.ACC_ANYROOMOWNER) + && !habbo.hasPermission(Permission.ACC_ENTERANYROOM)) { + BuildersClubRoomSupport.sendVisitDeniedOwnerBubble(room.getOwnerId(), habbo.getHabboInfo().getUsername()); + BuildersClubRoomSupport.sendVisitDeniedVisitorAlert(habbo.getHabboInfo().getId()); + habbo.getClient().sendResponse(new HotelViewComposer()); + habbo.getHabboInfo().setLoadingRoom(0); + return; + } + if (habbo.getHabboInfo().getRoomQueueId() != roomId) { Room queRoom = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); @@ -782,6 +794,7 @@ public class RoomManager { habbo.getRoomUnit().setInvisible(false); room.addHabbo(habbo); + BuildersClubRoomSupport.sendCurrentRoomPlacementStatus(room); room.getUserVariableManager().restorePermanentAssignments(habbo); // Pre-send own wearing badges so the client cache is populated before the user clicks themselves @@ -1018,6 +1031,7 @@ public class RoomManager { this.logExit(habbo); room.removeHabbo(habbo, true); + BuildersClubRoomSupport.sendCurrentRoomPlacementStatus(room); if (redirectToHotelView) { habbo.getClient().sendResponse(new HotelViewComposer()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java index f21b5289..b50edef8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java @@ -2,6 +2,8 @@ package com.eu.habbo.habbohotel.rooms; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildMember; +import com.eu.habbo.habbohotel.guilds.GuildRank; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.outgoing.rooms.RoomAddRightsListComposer; @@ -90,11 +92,16 @@ public class RoomRightsManager { */ public RoomRightLevels getGuildRightLevel(Habbo habbo) { int guildId = this.room.getGuildId(); - if (guildId > 0 && habbo.getHabboStats().hasGuild(guildId)) { + if (guildId > 0 && habbo != null && habbo.getHabboInfo() != null) { Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); - if (Emulator.getGameEnvironment().getGuildManager().getOnlyAdmins(guild) - .get(habbo.getHabboInfo().getId()) != null) { + if (guild == null) { + return RoomRightLevels.NONE; + } + + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild.getId(), habbo.getHabboInfo().getId()); + + if ((member != null) && (member.getRank() == GuildRank.ADMIN || member.getRank() == GuildRank.OWNER)) { return RoomRightLevels.GUILD_ADMIN; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java index 74d59e75..b760e4de 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java @@ -99,6 +99,7 @@ public class HabboStats implements Runnable { public int maxRooms; public int lastHCPayday; public int hcGiftsClaimed; + public int buildersClubBonusFurni; public int hcMessageLastModified = Emulator.getIntUnixTimestamp(); public THashSet subscriptions; @@ -155,6 +156,7 @@ public class HabboStats implements Runnable { this.maxRooms = set.getInt("max_rooms"); this.lastHCPayday = set.getInt("last_hc_payday"); this.hcGiftsClaimed = set.getInt("hc_gifts_claimed"); + this.buildersClubBonusFurni = set.getInt("builders_club_bonus_furni"); this.nuxReward = this.nux; @@ -327,7 +329,7 @@ public class HabboStats implements Runnable { int onlineTime = Emulator.getIntUnixTimestamp() - onlineTimeLast; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET achievement_score = ?, respects_received = ?, respects_given = ?, daily_respect_points = ?, block_following = ?, block_friendrequests = ?, online_time = online_time + ?, guild_id = ?, daily_pet_respect_points = ?, club_expire_timestamp = ?, login_streak = ?, rent_space_id = ?, rent_space_endtime = ?, volume_system = ?, volume_furni = ?, volume_trax = ?, block_roominvites = ?, old_chat = ?, block_camera_follow = ?, chat_color = ?, hof_points = ?, block_alerts = ?, talent_track_citizenship_level = ?, talent_track_helpers_level = ?, ignore_bots = ?, ignore_pets = ?, nux = ?, mute_end_timestamp = ?, allow_name_change = ?, perk_trade = ?, can_trade = ?, `forums_post_count` = ?, ui_flags = ?, has_gotten_default_saved_searches = ?, max_friends = ?, max_rooms = ?, last_hc_payday = ?, hc_gifts_claimed = ? WHERE user_id = ? LIMIT 1")) { + try (PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET achievement_score = ?, respects_received = ?, respects_given = ?, daily_respect_points = ?, block_following = ?, block_friendrequests = ?, online_time = online_time + ?, guild_id = ?, daily_pet_respect_points = ?, club_expire_timestamp = ?, login_streak = ?, rent_space_id = ?, rent_space_endtime = ?, volume_system = ?, volume_furni = ?, volume_trax = ?, block_roominvites = ?, old_chat = ?, block_camera_follow = ?, chat_color = ?, hof_points = ?, block_alerts = ?, talent_track_citizenship_level = ?, talent_track_helpers_level = ?, ignore_bots = ?, ignore_pets = ?, nux = ?, mute_end_timestamp = ?, allow_name_change = ?, perk_trade = ?, can_trade = ?, `forums_post_count` = ?, ui_flags = ?, has_gotten_default_saved_searches = ?, max_friends = ?, max_rooms = ?, last_hc_payday = ?, hc_gifts_claimed = ?, builders_club_bonus_furni = ? WHERE user_id = ? LIMIT 1")) { statement.setInt(1, this.achievementScore); statement.setInt(2, this.respectPointsReceived); statement.setInt(3, this.respectPointsGiven); @@ -366,7 +368,8 @@ public class HabboStats implements Runnable { statement.setInt(36, this.maxRooms); statement.setInt(37, this.lastHCPayday); statement.setInt(38, this.hcGiftsClaimed); - statement.setInt(39, this.habboInfo.getId()); + statement.setInt(39, this.buildersClubBonusFurni); + statement.setInt(40, this.habboInfo.getId()); statement.executeUpdate(); } @@ -436,6 +439,10 @@ public class HabboStats implements Runnable { } public int getAchievementProgress(Achievement achievement) { + if (achievement == null) { + return 0; + } + if (this.achievementProgress.containsKey(achievement)) return this.achievementProgress.get(achievement); @@ -575,6 +582,18 @@ public class HabboStats implements Runnable { return totalGifts - this.hcGiftsClaimed; } + public int getBuildersClubBonusFurni() { + return this.buildersClubBonusFurni; + } + + public void addBuildersClubBonusFurni(int amount) { + if (amount <= 0) { + return; + } + + this.buildersClubBonusFurni += amount; + } + public THashMap getAchievementProgress() { return this.achievementProgress; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java index 9589a892..5ada79ca 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java @@ -39,7 +39,7 @@ public class ItemsComponent { public static THashMap loadItems(Habbo habbo) { THashMap itemsList = new THashMap<>(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM items WHERE room_id = ? AND user_id = ?")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items.* FROM items LEFT JOIN builders_club_items ON builders_club_items.item_id = items.id WHERE items.room_id = ? AND items.user_id = ? AND builders_club_items.item_id IS NULL")) { statement.setInt(1, 0); statement.setInt(2, habbo.getHabboInfo().getId()); try (ResultSet set = statement.executeQuery()) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java index 08fba035..96ede18c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java @@ -11,6 +11,7 @@ import java.sql.SQLException; */ public class Subscription { public static final String HABBO_CLUB = "HABBO_CLUB"; + public static final String BUILDERS_CLUB = "BUILDERS_CLUB"; private final int id; private final int userId; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionBuildersClub.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionBuildersClub.java new file mode 100644 index 00000000..d25eb4c3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionBuildersClub.java @@ -0,0 +1,50 @@ +package com.eu.habbo.habbohotel.users.subscriptions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.users.Habbo; + +public class SubscriptionBuildersClub extends Subscription { + public SubscriptionBuildersClub(Integer id, Integer userId, String subscriptionType, Integer timestampStart, Integer duration, Boolean active) { + super(id, userId, subscriptionType, timestampStart, duration, active); + } + + @Override + public void onCreated() { + super.onCreated(); + if (BuildersClubRoomSupport.syncOwnedRooms(this.getUserId()) > 0) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(this.getUserId()); + } + BuildersClubRoomSupport.sendMembershipMadeBubble(this.getUserId()); + this.sendStatus(); + } + + @Override + public void onExtended(int duration) { + super.onExtended(duration); + if (BuildersClubRoomSupport.syncOwnedRooms(this.getUserId()) > 0) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(this.getUserId()); + } + BuildersClubRoomSupport.sendMembershipExtendedBubble(this.getUserId()); + this.sendStatus(); + } + + @Override + public void onExpired() { + super.onExpired(); + BuildersClubRoomSupport.syncOwnedRooms(this.getUserId()); + BuildersClubRoomSupport.sendMembershipExpiredAlert( + this.getUserId(), + BuildersClubRoomSupport.hasTrackedItemsInOwnedRooms(this.getUserId()) + ); + this.sendStatus(); + } + + private void sendStatus() { + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(this.getUserId()); + + if (habbo != null && habbo.getClient() != null) { + BuildersClubRoomSupport.sendPlacementStatus(habbo); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java index b39d9d52..333eee44 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java @@ -28,6 +28,7 @@ public class SubscriptionManager { public void init() { this.types.put(Subscription.HABBO_CLUB, SubscriptionHabboClub.class); + this.types.put(Subscription.BUILDERS_CLUB, SubscriptionBuildersClub.class); } public void addSubscriptionType(String type, Class clazz) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index c3fe35d4..a66c77e4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -240,7 +240,9 @@ public class PacketManager { this.registerHandler(Incoming.RequestGiftConfigurationEvent, RequestGiftConfigurationEvent.class); this.registerHandler(Incoming.GetMarketplaceConfigEvent, RequestMarketplaceConfigEvent.class); this.registerHandler(Incoming.RequestCatalogModeEvent, RequestCatalogModeEvent.class); - this.registerHandler(Incoming.RequestCatalogIndexEvent, RequestCatalogIndexEvent.class); + this.registerHandler(Incoming.BuildersClubQueryFurniCountEvent, BuildersClubQueryFurniCountEvent.class); + this.registerHandler(Incoming.BuildersClubPlaceRoomItemEvent, BuildersClubPlaceRoomItemEvent.class); + this.registerHandler(Incoming.BuildersClubPlaceWallItemEvent, BuildersClubPlaceWallItemEvent.class); this.registerHandler(Incoming.RequestCatalogPageEvent, RequestCatalogPageEvent.class); this.registerHandler(Incoming.CatalogBuyItemAsGiftEvent, CatalogBuyItemAsGiftEvent.class); this.registerHandler(Incoming.CatalogBuyItemEvent, CatalogBuyItemEvent.class); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 2c1aad0a..cd672c70 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -84,6 +84,9 @@ public class Incoming { public static final int RequestRecylerLogicEvent = 398; public static final int RequestGuildJoinEvent = 998; public static final int RequestCatalogIndexEvent = 2529; + public static final int BuildersClubQueryFurniCountEvent = 2529; + public static final int BuildersClubPlaceRoomItemEvent = 1051; + public static final int BuildersClubPlaceWallItemEvent = 462; public static final int RequestInventoryPetsEvent = 3095; public static final int ModToolRequestRoomVisitsEvent = 3526; public static final int ModToolWarnEvent = -1;//3763 diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java new file mode 100644 index 00000000..622677c3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java @@ -0,0 +1,158 @@ +package com.eu.habbo.messages.incoming.catalog; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogItem; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; + +import java.util.Iterator; + +public class BuildersClubPlaceRoomItemEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int pageId = this.packet.readInt(); + int offerId = this.packet.readInt(); + String extraData = this.packet.readString(); + short x = this.packet.readInt().shortValue(); + short y = this.packet.readInt().shortValue(); + int rotation = this.packet.readInt(); + + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + int placementUserId = BuildersClubRoomSupport.getPlacementPoolUserId(this.client.getHabbo()); + + if (room == null || !this.client.getHabbo().getRoomUnit().isInRoom()) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.canPlaceInCurrentRoom(this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.not_group_admin")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (placementUserId <= 0) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.hasActiveMembership(this.client.getHabbo().getHabboInfo().getId())) { + int trackedFurniCount = BuildersClubRoomSupport.getTrackedFurniCount(placementUserId); + + if (trackedFurniCount >= BuildersClubRoomSupport.getFurniLimit(placementUserId)) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "room.error.max_furniture")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (BuildersClubRoomSupport.hasPlacementVisitors(room, this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.visitors")); + return; + } + } + + CatalogItem catalogItem = resolveCatalogItem(pageId, offerId); + Item baseItem = resolveBaseItem(catalogItem, FurnitureType.FLOOR); + + if (catalogItem == null || baseItem == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + RoomTile tile = room.getLayout().getTile(x, y); + + if (tile == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(BuildersClubRoomSupport.VIRTUAL_OWNER_ID, 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; + } + + FurnitureMovementError error = room.canPlaceFurnitureAt(item, this.client.getHabbo(), tile, rotation); + + if (!error.equals(FurnitureMovementError.NONE)) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); + return; + } + + error = room.placeFloorFurniAt(item, tile, rotation, this.client.getHabbo()); + + if (!error.equals(FurnitureMovementError.NONE)) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); + return; + } + + BuildersClubRoomSupport.trackPlacedItem(item.getId(), placementUserId, room.getId()); + + if (BuildersClubRoomSupport.syncRoom(room) == BuildersClubRoomSupport.SyncResult.LOCKED) { + BuildersClubRoomSupport.sendRoomLockedBubble(room.getOwnerId()); + } + + BuildersClubRoomSupport.sendPlacementStatusForPool(room, placementUserId); + } + + private CatalogItem resolveCatalogItem(int pageId, int offerId) { + CatalogItem buildersClubItem = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(offerId, CatalogPageType.BUILDER); + + if (buildersClubItem != null) { + return buildersClubItem; + } + + int catalogItemId = Emulator.getGameEnvironment().getCatalogManager().offerDefs.get(offerId); + + if (catalogItemId > 0) { + return Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(catalogItemId); + } + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, CatalogPageType.BUILDER); + + if (page == null) { + return null; + } + + for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) { + if (catalogItem.getOfferId() == offerId) { + return catalogItem; + } + } + + return null; + } + + private Item resolveBaseItem(CatalogItem catalogItem, FurnitureType expectedType) { + if (catalogItem == null || catalogItem.getAmount() != 1 || catalogItem.getBaseItems().size() != 1) { + return null; + } + + Iterator iterator = catalogItem.getBaseItems().iterator(); + + if (!iterator.hasNext()) { + return null; + } + + Item baseItem = iterator.next(); + + if (baseItem == null || baseItem.getType() != expectedType) { + return null; + } + + return baseItem; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java new file mode 100644 index 00000000..96b29a7c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java @@ -0,0 +1,140 @@ +package com.eu.habbo.messages.incoming.catalog; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogItem; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; + +import java.util.Iterator; + +public class BuildersClubPlaceWallItemEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int pageId = this.packet.readInt(); + int offerId = this.packet.readInt(); + String extraData = this.packet.readString(); + String wallPosition = this.packet.readString(); + + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + int placementUserId = BuildersClubRoomSupport.getPlacementPoolUserId(this.client.getHabbo()); + + if (room == null || !this.client.getHabbo().getRoomUnit().isInRoom()) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.canPlaceInCurrentRoom(this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.not_group_admin")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (placementUserId <= 0) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.hasActiveMembership(this.client.getHabbo().getHabboInfo().getId())) { + int trackedFurniCount = BuildersClubRoomSupport.getTrackedFurniCount(placementUserId); + + if (trackedFurniCount >= BuildersClubRoomSupport.getFurniLimit(placementUserId)) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "room.error.max_furniture")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (BuildersClubRoomSupport.hasPlacementVisitors(room, this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.visitors")); + return; + } + } + + CatalogItem catalogItem = resolveCatalogItem(pageId, offerId); + Item baseItem = resolveBaseItem(catalogItem, FurnitureType.WALL); + + if (catalogItem == null || baseItem == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(BuildersClubRoomSupport.VIRTUAL_OWNER_ID, 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; + } + + FurnitureMovementError error = room.placeWallFurniAt(item, wallPosition, this.client.getHabbo()); + + if (!error.equals(FurnitureMovementError.NONE)) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); + return; + } + + BuildersClubRoomSupport.trackPlacedItem(item.getId(), placementUserId, room.getId()); + + if (BuildersClubRoomSupport.syncRoom(room) == BuildersClubRoomSupport.SyncResult.LOCKED) { + BuildersClubRoomSupport.sendRoomLockedBubble(room.getOwnerId()); + } + + BuildersClubRoomSupport.sendPlacementStatusForPool(room, placementUserId); + } + + private CatalogItem resolveCatalogItem(int pageId, int offerId) { + CatalogItem buildersClubItem = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(offerId, CatalogPageType.BUILDER); + + if (buildersClubItem != null) { + return buildersClubItem; + } + + int catalogItemId = Emulator.getGameEnvironment().getCatalogManager().offerDefs.get(offerId); + + if (catalogItemId > 0) { + return Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(catalogItemId); + } + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, CatalogPageType.BUILDER); + + if (page == null) { + return null; + } + + for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) { + if (catalogItem.getOfferId() == offerId) { + return catalogItem; + } + } + + return null; + } + + private Item resolveBaseItem(CatalogItem catalogItem, FurnitureType expectedType) { + if (catalogItem == null || catalogItem.getAmount() != 1 || catalogItem.getBaseItems().size() != 1) { + return null; + } + + Iterator iterator = catalogItem.getBaseItems().iterator(); + + if (!iterator.hasNext()) { + return null; + } + + Item baseItem = iterator.next(); + + if (baseItem == null || baseItem.getType() != expectedType) { + return null; + } + + return baseItem; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubQueryFurniCountEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubQueryFurniCountEvent.java new file mode 100644 index 00000000..8f733776 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubQueryFurniCountEvent.java @@ -0,0 +1,11 @@ +package com.eu.habbo.messages.incoming.catalog; + +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class BuildersClubQueryFurniCountEvent extends MessageHandler { + @Override + public void handle() throws Exception { + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java index eb1086e3..e4aac1b0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java @@ -7,6 +7,7 @@ import com.eu.habbo.habbohotel.catalog.layouts.*; import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.pets.PetManager; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.habbohotel.rooms.RoomManager; import com.eu.habbo.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.users.HabboInventory; @@ -14,6 +15,8 @@ import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseFailedComposer; import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseUnavailableComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; import com.eu.habbo.messages.outgoing.catalog.PurchaseOKComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; @@ -139,10 +142,10 @@ public class CatalogBuyItemEvent extends MessageHandler { return; } - if (page instanceof ClubBuyLayout || page instanceof VipBuyLayout) { + if (this.isClubOfferPage(page)) { ClubOffer item = Emulator.getGameEnvironment().getCatalogManager().clubOffers.get(itemId); - if (item == null) { + if (item == null || !item.belongsToWindow(this.getClubOfferWindowId(page))) { this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } @@ -170,20 +173,19 @@ public class CatalogBuyItemEvent extends MessageHandler { if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) this.client.getHabbo().givePoints(item.getPointsType(), -totalDuckets); + if (item.isBuildersClubAddon()) { + this.client.getHabbo().getHabboStats().addBuildersClubBonusFurni(totalDays); + this.client.sendResponse(new BuildersClubFurniCountComposer(BuildersClubRoomSupport.getTrackedFurniCount(this.client.getHabbo().getHabboInfo().getId()))); + this.client.sendResponse(new BuildersClubSubscriptionStatusComposer(this.client.getHabbo())); + } else { + String subscriptionType = item.isBuildersClubSubscription() ? Subscription.BUILDERS_CLUB : Subscription.HABBO_CLUB; - if(this.client.getHabbo().getHabboStats().createSubscription(Subscription.HABBO_CLUB, (totalDays * 86400)) == null) { - this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); - throw new Exception("Unable to create or extend subscription"); + if (this.client.getHabbo().getHabboStats().createSubscription(subscriptionType, (totalDays * 86400)) == null) { + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); + throw new Exception("Unable to create or extend subscription"); + } } - /*if (this.client.getHabbo().getHabboStats().getClubExpireTimestamp() <= Emulator.getIntUnixTimestamp()) - this.client.getHabbo().getHabboStats().setClubExpireTimestamp(Emulator.getIntUnixTimestamp()); - - this.client.getHabbo().getHabboStats().setClubExpireTimestamp(this.client.getHabbo().getHabboStats().getClubExpireTimestamp() + (totalDays * 86400)); - - this.client.sendResponse(new UserPermissionsComposer(this.client.getHabbo())); - this.client.sendResponse(new UserClubComposer(this.client.getHabbo()));*/ - this.client.sendResponse(new PurchaseOKComposer(null)); this.client.sendResponse(new InventoryRefreshComposer()); @@ -223,4 +225,24 @@ public class CatalogBuyItemEvent extends MessageHandler { this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); } } + + private boolean isClubOfferPage(CatalogPage page) { + return page instanceof ClubBuyLayout + || page instanceof VipBuyLayout + || page instanceof BuildersClubFrontPageLayout + || page instanceof BuildersClubAddonsLayout + || page instanceof BuildersClubLoyaltyLayout; + } + + private int getClubOfferWindowId(CatalogPage page) { + if (page instanceof BuildersClubAddonsLayout) { + return ClubOffer.WINDOW_BUILDERS_CLUB_ADDONS; + } + + if (page instanceof BuildersClubFrontPageLayout || page instanceof BuildersClubLoyaltyLayout) { + return ClubOffer.WINDOW_BUILDERS_CLUB; + } + + return ClubOffer.WINDOW_HABBO_CLUB; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java index 9e54083a..c40c81f3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java @@ -1,20 +1,17 @@ package com.eu.habbo.messages.incoming.catalog; import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.messages.outgoing.catalog.CatalogModeComposer; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.messages.outgoing.catalog.CatalogPagesListComposer; public class RequestCatalogModeEvent extends MessageHandler { @Override public void handle() throws Exception { - String MODE = this.packet.readString(); - if (MODE.equalsIgnoreCase("normal")) { - this.client.sendResponse(new CatalogModeComposer(0)); - this.client.sendResponse(new CatalogPagesListComposer(this.client.getHabbo(), MODE)); - } else { - this.client.sendResponse(new CatalogModeComposer(1)); - this.client.sendResponse(new CatalogPagesListComposer(this.client.getHabbo(), MODE)); + this.client.sendResponse(new CatalogPagesListComposer(this.client.getHabbo(), MODE)); + + if (!MODE.equalsIgnoreCase("normal")) { + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java index 0198c01e..bf2ba215 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.catalog; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.modtool.ScripterManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.CatalogPageComposer; @@ -13,8 +14,9 @@ public class RequestCatalogPageEvent extends MessageHandler { int catalogPageId = this.packet.readInt(); int offerId = this.packet.readInt(); String mode = this.packet.readString(); + CatalogPageType requestedType = CatalogPageType.fromString(mode); - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(catalogPageId); + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(catalogPageId, requestedType); if (catalogPageId > 0 && page != null) { if (page.getRank() <= this.client.getHabbo().getHabboInfo().getRank().getId() && page.isEnabled()) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java index e8683614..de26a442 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -32,26 +33,35 @@ public class CatalogAdminCreateOfferEvent extends MessageHandler { int offerIdGroup = this.packet.readInt(); int limitedStack = this.packet.readInt(); int orderNumber = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); int newId = -1; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, club_only, extradata, have_offer, offer_id, limited_stack, order_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (pageType == CatalogPageType.BUILDER) + ? "INSERT INTO catalog_items_bc (page_id, item_ids, catalog_name, order_number, extradata) VALUES (?, ?, ?, ?, ?)" + : "INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, club_only, extradata, have_offer, offer_id, limited_stack, order_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { statement.setInt(1, pageId); statement.setString(2, String.valueOf(itemId)); statement.setString(3, catalogName); - statement.setInt(4, costCredits); - statement.setInt(5, costPoints); - statement.setInt(6, pointsType); - statement.setInt(7, amount); - statement.setString(8, clubOnly == 1 ? "1" : "0"); - statement.setString(9, extradata); - statement.setString(10, haveOffer ? "1" : "0"); - statement.setInt(11, offerIdGroup); - statement.setInt(12, limitedStack); - statement.setInt(13, orderNumber); + + if (pageType == CatalogPageType.BUILDER) { + statement.setInt(4, orderNumber); + statement.setString(5, extradata); + } else { + statement.setInt(4, costCredits); + statement.setInt(5, costPoints); + statement.setInt(6, pointsType); + statement.setInt(7, amount); + statement.setString(8, clubOnly == 1 ? "1" : "0"); + statement.setString(9, extradata); + statement.setString(10, haveOffer ? "1" : "0"); + statement.setInt(11, offerIdGroup); + statement.setInt(12, limitedStack); + statement.setInt(13, orderNumber); + } statement.execute(); try (ResultSet keys = statement.getGeneratedKeys()) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java index a6d1c795..598582ad 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java @@ -3,6 +3,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogPage; import com.eu.habbo.habbohotel.catalog.CatalogPageLayouts; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -25,6 +26,8 @@ public class CatalogAdminCreatePageEvent extends MessageHandler { boolean enabled = this.packet.readBoolean(); int orderNum = this.packet.readInt(); int parentId = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + CatalogPageType catalogMode = CatalogPageType.fromString(this.packet.readString()); CatalogPageLayouts pageLayout; try { @@ -34,7 +37,7 @@ public class CatalogAdminCreatePageEvent extends MessageHandler { } CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage( - caption, caption2, 0, iconType, pageLayout, minRank, parentId + caption, caption2, 0, iconType, pageLayout, minRank, parentId, pageType, catalogMode ); if (page == null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeleteOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeleteOfferEvent.java index f0dd04aa..1363af8c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeleteOfferEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeleteOfferEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -18,9 +19,10 @@ public class CatalogAdminDeleteOfferEvent extends MessageHandler { } int offerId = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("DELETE FROM catalog_items WHERE id = ?")) { + PreparedStatement statement = connection.prepareStatement((pageType == CatalogPageType.BUILDER) ? "DELETE FROM catalog_items_bc WHERE id = ?" : "DELETE FROM catalog_items WHERE id = ?")) { statement.setInt(1, offerId); statement.execute(); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java index 7be13a40..c72f0273 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -19,21 +20,26 @@ public class CatalogAdminDeletePageEvent extends MessageHandler { } int pageId = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId); + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType); if (page == null) { this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); return; } + String query = (pageType == CatalogPageType.BUILDER) + ? "DELETE FROM catalog_pages_bc WHERE id = ?" + : "DELETE FROM catalog_pages WHERE id = ?"; + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("DELETE FROM catalog_pages WHERE id = ?")) { + PreparedStatement statement = connection.prepareStatement(query)) { statement.setInt(1, pageId); statement.execute(); } - Emulator.getGameEnvironment().getCatalogManager().catalogPages.remove(pageId); + Emulator.getGameEnvironment().getCatalogManager().getCatalogPagesMap(pageType).remove(pageId); this.client.sendResponse(new CatalogAdminResultComposer(true, "Page deleted")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMoveOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMoveOfferEvent.java index e2341011..f6be0130 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMoveOfferEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMoveOfferEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -19,9 +20,10 @@ public class CatalogAdminMoveOfferEvent extends MessageHandler { int offerId = this.packet.readInt(); int orderNumber = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("UPDATE catalog_items SET order_number = ? WHERE id = ?")) { + PreparedStatement statement = connection.prepareStatement((pageType == CatalogPageType.BUILDER) ? "UPDATE catalog_items_bc SET order_number = ? WHERE id = ?" : "UPDATE catalog_items SET order_number = ? WHERE id = ?")) { statement.setInt(1, orderNumber); statement.setInt(2, offerId); statement.execute(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java index 628bff77..20be1400 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -20,13 +21,15 @@ public class CatalogAdminMovePageEvent extends MessageHandler { int pageId = this.packet.readInt(); int newParentId = this.packet.readInt(); int newIndex = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages"; // Special values: -1 = toggle enabled, -2 = toggle visible if (newParentId == -1) { // Toggle enabled try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE catalog_pages SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) { + "UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) { statement.setInt(1, pageId); statement.execute(); } @@ -38,7 +41,7 @@ public class CatalogAdminMovePageEvent extends MessageHandler { // Toggle visible try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE catalog_pages SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) { + "UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) { statement.setInt(1, pageId); statement.execute(); } @@ -49,7 +52,7 @@ public class CatalogAdminMovePageEvent extends MessageHandler { // Normal move: update parent and order try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE catalog_pages SET parent_id = ?, order_num = ? WHERE id = ?")) { + "UPDATE " + tableName + " SET parent_id = ?, order_num = ? WHERE id = ?")) { statement.setInt(1, newParentId); statement.setInt(2, newIndex); statement.setInt(3, pageId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java index 471b737c..ca45e8ce 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -31,24 +32,34 @@ public class CatalogAdminSaveOfferEvent extends MessageHandler { int offerIdGroup = this.packet.readInt(); int limitedStack = this.packet.readInt(); int orderNumber = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE catalog_items SET page_id = ?, item_ids = ?, catalog_name = ?, cost_credits = ?, cost_points = ?, points_type = ?, amount = ?, club_only = ?, extradata = ?, have_offer = ?, offer_id = ?, limited_stack = ?, order_number = ? WHERE id = ?")) { + (pageType == CatalogPageType.BUILDER) + ? "UPDATE catalog_items_bc SET page_id = ?, item_ids = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?" + : "UPDATE catalog_items SET page_id = ?, item_ids = ?, catalog_name = ?, cost_credits = ?, cost_points = ?, points_type = ?, amount = ?, club_only = ?, extradata = ?, have_offer = ?, offer_id = ?, limited_stack = ?, order_number = ? WHERE id = ?")) { statement.setInt(1, pageId); statement.setString(2, String.valueOf(itemId)); statement.setString(3, catalogName); - statement.setInt(4, costCredits); - statement.setInt(5, costPoints); - statement.setInt(6, pointsType); - statement.setInt(7, amount); - statement.setString(8, clubOnly == 1 ? "1" : "0"); - statement.setString(9, extradata); - statement.setString(10, haveOffer ? "1" : "0"); - statement.setInt(11, offerIdGroup); - statement.setInt(12, limitedStack); - statement.setInt(13, orderNumber); - statement.setInt(14, offerId); + + if (pageType == CatalogPageType.BUILDER) { + statement.setInt(4, orderNumber); + statement.setString(5, extradata); + statement.setInt(6, offerId); + } else { + statement.setInt(4, costCredits); + statement.setInt(5, costPoints); + statement.setInt(6, pointsType); + statement.setInt(7, amount); + statement.setString(8, clubOnly == 1 ? "1" : "0"); + statement.setString(9, extradata); + statement.setString(10, haveOffer ? "1" : "0"); + statement.setInt(11, offerIdGroup); + statement.setInt(12, limitedStack); + statement.setInt(13, orderNumber); + statement.setInt(14, offerId); + } statement.execute(); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java index 1caa18b3..7c8ab7ed 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; @@ -31,30 +32,51 @@ public class CatalogAdminSavePageEvent extends MessageHandler { String headline = this.packet.readString(); String teaser = this.packet.readString(); String textDetails = this.packet.readString(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + CatalogPageType catalogMode = CatalogPageType.fromString(this.packet.readString()); - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId); + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType); if (page == null) { this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); return; } + String query = (pageType == CatalogPageType.BUILDER) + ? "UPDATE catalog_pages_bc SET caption = ?, page_layout = ?, icon_image = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ? WHERE id = ?" + : "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, catalog_mode = ? WHERE id = ?"; + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement( - "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ? WHERE id = ?")) { + PreparedStatement statement = connection.prepareStatement(query)) { statement.setString(1, caption); - statement.setString(2, caption2); - statement.setString(3, layout); - statement.setInt(4, iconType); - statement.setInt(5, minRank); - statement.setString(6, visible ? "1" : "0"); - statement.setString(7, enabled ? "1" : "0"); - statement.setInt(8, orderNum); - statement.setInt(9, parentId); - statement.setString(10, headline); - statement.setString(11, teaser); - statement.setString(12, textDetails); - statement.setInt(13, pageId); + + if (pageType == CatalogPageType.BUILDER) { + statement.setString(2, layout); + statement.setInt(3, iconType); + statement.setString(4, visible ? "1" : "0"); + statement.setString(5, enabled ? "1" : "0"); + statement.setInt(6, orderNum); + statement.setInt(7, parentId); + statement.setString(8, headline); + statement.setString(9, teaser); + statement.setString(10, textDetails); + statement.setInt(11, pageId); + } else { + statement.setString(2, caption2); + statement.setString(3, layout); + statement.setInt(4, iconType); + statement.setInt(5, minRank); + statement.setString(6, visible ? "1" : "0"); + statement.setString(7, enabled ? "1" : "0"); + statement.setInt(8, orderNum); + statement.setInt(9, parentId); + statement.setString(10, headline); + statement.setString(11, teaser); + statement.setString(12, textDetails); + statement.setString(13, catalogMode.name()); + statement.setInt(14, pageId); + } + statement.execute(); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java index a1ce8c48..80677246 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java @@ -7,7 +7,6 @@ import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.guilds.GuildFavoriteRoomUserUpdateComposer; -import com.eu.habbo.messages.outgoing.guilds.RemoveGuildFromRoomComposer; import com.eu.habbo.messages.outgoing.rooms.RoomDataComposer; import com.eu.habbo.plugin.events.guilds.GuildDeletedEvent; import gnu.trove.set.hash.THashSet; @@ -38,11 +37,15 @@ public class GuildDeleteEvent extends MessageHandler { Emulator.getGameEnvironment().getGuildManager().deleteGuild(guild); Emulator.getPluginManager().fireEvent(new GuildDeletedEvent(guild, this.client.getHabbo())); - Emulator.getGameEnvironment().getRoomManager().getRoom(guild.getRoomId()).sendComposer(new RemoveGuildFromRoomComposer(guildId).compose()); + com.eu.habbo.habbohotel.rooms.Room guildRoom = Emulator.getGameEnvironment().getRoomManager().getRoom(guild.getRoomId()); - if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { - if (guild.getRoomId() == this.client.getHabbo().getHabboInfo().getCurrentRoom().getId()) { - this.client.sendResponse(new RoomDataComposer(this.client.getHabbo().getHabboInfo().getCurrentRoom(), this.client.getHabbo(), false, false)); + if (guildRoom != null) { + for (Habbo habbo : guildRoom.getHabbos()) { + if (habbo.getClient() == null) { + continue; + } + + habbo.getClient().sendResponse(new RoomDataComposer(guildRoom, habbo, true, false)); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java index a6eeeef0..0171da26 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java @@ -12,6 +12,7 @@ import com.eu.habbo.messages.outgoing.catalog.PurchaseOKComposer; import com.eu.habbo.messages.outgoing.guilds.GuildBoughtComposer; import com.eu.habbo.messages.outgoing.guilds.GuildEditFailComposer; import com.eu.habbo.messages.outgoing.guilds.GuildInfoComposer; +import com.eu.habbo.messages.outgoing.rooms.RoomDataComposer; import com.eu.habbo.plugin.events.guilds.GuildPurchasedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,20 +97,29 @@ public class RequestGuildBuyEvent extends MessageHandler { r.removeAllRights(); r.setNeedsUpdate(true); + Emulator.getGameEnvironment().getGuildManager().addGuild(guild); + if (Emulator.getConfig().getBoolean("imager.internal.enabled")) { Emulator.getBadgeImager().generate(guild); } this.client.sendResponse(new PurchaseOKComposer()); this.client.sendResponse(new GuildBoughtComposer(guild)); - for (Habbo habbo : r.getHabbos()) { - habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null)); - } r.refreshGuild(guild); - Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo())); + for (Habbo habbo : r.getHabbos()) { + if (habbo.getClient() == null) { + continue; + } - Emulator.getGameEnvironment().getGuildManager().addGuild(guild); + habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null)); + + if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) { + habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false)); + } + } + + Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo())); } } else { String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName())); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java index 205f3b39..13d47c28 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java @@ -10,13 +10,16 @@ import com.eu.habbo.habbohotel.navigation.NavigatorSavedSearch; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomManager; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.habbohotel.users.clothingvalidation.ClothingValidationManager; +import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionHabboClub; import com.eu.habbo.messages.NoAuthMessage; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; import com.eu.habbo.messages.outgoing.commands.AvailableCommandsComposer; import com.eu.habbo.messages.outgoing.gamecenter.GameCenterAccountInfoComposer; import com.eu.habbo.messages.outgoing.gamecenter.GameCenterGameListComposer; @@ -34,7 +37,6 @@ import com.eu.habbo.messages.outgoing.modtool.ModToolComposer; import com.eu.habbo.messages.outgoing.modtool.ModToolSanctionInfoComposer; import com.eu.habbo.messages.outgoing.mysterybox.MysteryBoxKeysComposer; import com.eu.habbo.messages.outgoing.navigator.NewNavigatorSavedSearchesComposer; -import com.eu.habbo.messages.outgoing.unknown.BuildersClubExpiredComposer; import com.eu.habbo.messages.outgoing.users.*; import com.eu.habbo.plugin.events.emulator.SSOAuthenticationEvent; import com.eu.habbo.plugin.events.users.UserLoginEvent; @@ -220,7 +222,7 @@ public class SecureLoginEvent extends MessageHandler { messages.add(new UserAchievementScoreComposer(this.client.getHabbo()).compose()); messages.add(new IsFirstLoginOfDayComposer(true).compose()); messages.add(new MysteryBoxKeysComposer().compose()); - messages.add(new BuildersClubExpiredComposer().compose()); + messages.add(new BuildersClubSubscriptionStatusComposer(this.client.getHabbo()).compose()); messages.add(new CfhTopicsMessageComposer().compose()); messages.add(new FavoriteRoomsCountComposer(this.client.getHabbo()).compose()); messages.add(new GameCenterGameListComposer().compose()); @@ -235,6 +237,33 @@ public class SecureLoginEvent extends MessageHandler { this.client.sendResponses(messages); + if (!isSessionResume) { + BuildersClubRoomSupport.syncOwnedRooms(this.client.getHabbo().getHabboInfo().getId()); + + boolean hasActiveBuildersClub = this.client.getHabbo().getHabboStats().hasSubscription(Subscription.BUILDERS_CLUB); + boolean hadBuildersClubBefore = false; + + for (com.eu.habbo.habbohotel.users.subscriptions.Subscription subscription : this.client.getHabbo().getHabboStats().subscriptions) { + if (subscription.getSubscriptionType().equalsIgnoreCase(Subscription.BUILDERS_CLUB)) { + hadBuildersClubBefore = true; + break; + } + } + + if (hasActiveBuildersClub) { + int remaining = BuildersClubRoomSupport.getMembershipSecondsLeft(this.client.getHabbo().getHabboInfo().getId()); + + if (remaining > 0 && remaining <= (72 * 3600)) { + BuildersClubRoomSupport.sendMembershipExpiringAlert(this.client.getHabbo().getHabboInfo().getId()); + } + } else if (hadBuildersClubBefore) { + BuildersClubRoomSupport.sendMembershipExpiredAlert( + this.client.getHabbo().getHabboInfo().getId(), + BuildersClubRoomSupport.hasTrackedItemsInOwnedRooms(this.client.getHabbo().getHabboInfo().getId()) + ); + } + } + //Hardcoded //this.client.sendResponse(new ForumsTestComposer()); this.client.sendResponse(new InventoryAchievementsComposer()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java index 88d62d20..7c51b179 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java @@ -3,9 +3,12 @@ package com.eu.habbo.messages.incoming.rooms.items; import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.items.interactions.*; import com.eu.habbo.habbohotel.modtool.ScripterManager; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.habbohotel.rooms.*; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.inventory.RemoveHabboItemComposer; @@ -115,5 +118,29 @@ public class RoomPlaceItemEvent extends MessageHandler { this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId())); this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item.getId()); item.setFromGift(false); + + if (BuildersClubRoomSupport.isTrackedItem(item.getId())) { + int trackedUserId = BuildersClubRoomSupport.getTrackedUserId(item.getId()); + + if (trackedUserId <= 0) { + trackedUserId = this.client.getHabbo().getHabboInfo().getId(); + } + + item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); + BuildersClubRoomSupport.trackPlacedItem(item.getId(), trackedUserId, room.getId()); + + BuildersClubRoomSupport.SyncResult syncResult = BuildersClubRoomSupport.syncRoom(room); + + if (syncResult == BuildersClubRoomSupport.SyncResult.LOCKED) { + BuildersClubRoomSupport.sendRoomLockedBubble(room.getOwnerId()); + } else if (syncResult == BuildersClubRoomSupport.SyncResult.UNLOCKED) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(room.getOwnerId()); + } + + if (trackedUserId == this.client.getHabbo().getHabboInfo().getId()) { + this.client.sendResponse(new BuildersClubFurniCountComposer(BuildersClubRoomSupport.getTrackedFurniCount(trackedUserId))); + this.client.sendResponse(new BuildersClubSubscriptionStatusComposer(this.client.getHabbo())); + } + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java index c4c30c5f..96642103 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java @@ -43,6 +43,11 @@ public class UserSaveLookEvent extends MessageHandler { if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); } + this.client.getHabbo().getMessenger().connectionChanged( + this.client.getHabbo(), + this.client.getHabbo().isOnline(), + this.client.getHabbo().getHabboInfo().getCurrentRoom() != null + ); AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("AvatarLooks")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 36ef55a3..2b01ff81 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -376,6 +376,7 @@ public class Outgoing { public final static int BullyReportedMessageComposer = 3285; // PRODUCTION-201611291003-338511768 public final static int UnknownQuestComposer3 = 1122; // PRODUCTION-201611291003-338511768 public final static int FriendToolbarNotificationComposer = 3082; // PRODUCTION-201611291003-338511768 + public final static int SimpleAlertComposer = 5100; // PRODUCTION-201611291003-338511768 public final static int MessengerErrorComposer = 896; // PRODUCTION-201611291003-338511768 public final static int CameraPriceComposer = 3878; // PRODUCTION-201611291003-338511768 public final static int PetBreedingCompleted = 2527; // PRODUCTION-201611291003-338511768 diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubFurniCountComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubFurniCountComposer.java new file mode 100644 index 00000000..7861cf8d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubFurniCountComposer.java @@ -0,0 +1,20 @@ +package com.eu.habbo.messages.outgoing.catalog; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class BuildersClubFurniCountComposer extends MessageComposer { + private final int furniCount; + + public BuildersClubFurniCountComposer(int furniCount) { + this.furniCount = furniCount; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.CatalogModeComposer); + this.response.appendInt(this.furniCount); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubSubscriptionStatusComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubSubscriptionStatusComposer.java new file mode 100644 index 00000000..2e11b68f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubSubscriptionStatusComposer.java @@ -0,0 +1,48 @@ +package com.eu.habbo.messages.outgoing.catalog; + +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class BuildersClubSubscriptionStatusComposer extends MessageComposer { + private final int secondsLeft; + private final int furniLimit; + private final int maxFurniLimit; + private final int secondsLeftWithGrace; + private final boolean placementBlockedByVisitors; + private final boolean placementAllowedInCurrentRoom; + + public BuildersClubSubscriptionStatusComposer(Habbo habbo) { + this( + BuildersClubRoomSupport.getMembershipSecondsLeft(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.getFurniLimit(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.getFurniLimit(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.getMembershipSecondsLeft(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.isPlacementBlockedByVisitors(habbo), + BuildersClubRoomSupport.canPlaceInCurrentRoom(habbo) + ); + } + + public BuildersClubSubscriptionStatusComposer(int secondsLeft, int furniLimit, int maxFurniLimit, int secondsLeftWithGrace, boolean placementBlockedByVisitors, boolean placementAllowedInCurrentRoom) { + this.secondsLeft = Math.max(0, secondsLeft); + this.furniLimit = Math.max(0, furniLimit); + this.maxFurniLimit = Math.max(0, maxFurniLimit); + this.secondsLeftWithGrace = Math.max(0, secondsLeftWithGrace); + this.placementBlockedByVisitors = placementBlockedByVisitors; + this.placementAllowedInCurrentRoom = placementAllowedInCurrentRoom; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.BuildersClubExpiredComposer); + this.response.appendInt(this.secondsLeft); + this.response.appendInt(this.furniLimit); + this.response.appendInt(this.maxFurniLimit); + this.response.appendInt(this.secondsLeftWithGrace); + this.response.appendBoolean(this.placementBlockedByVisitors); + this.response.appendBoolean(this.placementAllowedInCurrentRoom); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java index 92c1bc5a..c0acdc53 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.outgoing.catalog; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.ServerMessage; @@ -32,7 +33,8 @@ public class CatalogPagesListComposer extends MessageComposer { @Override protected ServerMessage composeInternal() { try { - List pages = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(-1, this.habbo); + CatalogPageType requestedType = CatalogPageType.fromString(this.mode); + List pages = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(-1, this.habbo, requestedType); this.response.init(Outgoing.CatalogPagesListComposer); @@ -47,7 +49,7 @@ public class CatalogPagesListComposer extends MessageComposer { this.response.appendInt(childCount); for (int idx = 0; idx < childCount; idx++) { - this.append(pages.get(idx), 1); + this.append(pages.get(idx), 1, requestedType); } this.response.appendBoolean(false); @@ -61,8 +63,8 @@ public class CatalogPagesListComposer extends MessageComposer { return null; } - private void append(CatalogPage category, int depth) { - List pagesList = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(category.getId(), this.habbo); + private void append(CatalogPage category, int depth, CatalogPageType requestedType) { + List pagesList = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(category.getId(), this.habbo, requestedType); this.response.appendBoolean(category.isVisible()); this.response.appendInt(category.getIconImage()); @@ -87,7 +89,7 @@ public class CatalogPagesListComposer extends MessageComposer { this.response.appendInt(childCount); for (int idx = 0; idx < childCount; idx++) { - this.append(pagesList.get(idx), depth + 1); + this.append(pagesList.get(idx), depth + 1, requestedType); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java index 6a1ff748..070f9a5c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java @@ -3,6 +3,7 @@ package com.eu.habbo.messages.outgoing.catalog; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.ClubOffer; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -22,12 +23,15 @@ public class ClubDataComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.ClubDataComposer); - List offers = Emulator.getGameEnvironment().getCatalogManager().getClubOffers(); + List offers = Emulator.getGameEnvironment().getCatalogManager().getClubOffers(this.windowId); this.response.appendInt(offers.size()); - //TODO Change this to a seperate table. for (ClubOffer offer : offers) { - offer.serialize(this.response, this.habbo.getHabboStats().getClubExpireTimestamp()); + int expireTimestamp = offer.isBuildersClubSubscription() + ? this.habbo.getHabboStats().getSubscriptionExpireTimestamp(Subscription.BUILDERS_CLUB) + : (offer.isBuildersClubAddon() ? Emulator.getIntUnixTimestamp() : this.habbo.getHabboStats().getClubExpireTimestamp()); + + offer.serialize(this.response, expireTimestamp); } this.response.appendInt(this.windowId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/generic/alerts/SimpleAlertComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/generic/alerts/SimpleAlertComposer.java new file mode 100644 index 00000000..c8a7c8c7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/generic/alerts/SimpleAlertComposer.java @@ -0,0 +1,31 @@ +package com.eu.habbo.messages.outgoing.generic.alerts; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class SimpleAlertComposer extends MessageComposer { + private final String alertMessage; + private final String titleMessage; + + public SimpleAlertComposer(String alertMessage) { + this(alertMessage, null); + } + + public SimpleAlertComposer(String alertMessage, String titleMessage) { + this.alertMessage = alertMessage; + this.titleMessage = titleMessage; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.SimpleAlertComposer); + this.response.appendString(this.alertMessage); + + if (this.titleMessage != null && !this.titleMessage.isEmpty()) { + this.response.appendString(this.titleMessage); + } + + return this.response; + } +} diff --git a/docs/builders_club_catalog_reference.md b/docs/builders_club_catalog_reference.md new file mode 100644 index 00000000..d77961ae --- /dev/null +++ b/docs/builders_club_catalog_reference.md @@ -0,0 +1,284 @@ +# Builders Club Catalog Reference + +Questa guida riassume il setup corretto dopo la separazione completa tra catalogo normale e `Builders Club`. + +## Tabelle usate davvero + +- Catalogo normale: + - `catalog_pages` + - `catalog_items` +- Builders Club: + - `catalog_pages_bc` + - `catalog_items_bc` +- Abbonamenti / add-on BC venduti nel catalogo normale: + - `catalog_club_offers` + +Quindi: + +- se vuoi una pagina BC, va in `catalog_pages_bc` +- se vuoi un furni BC, va in `catalog_items_bc` +- se vuoi vendere lo stesso furni anche nel catalogo normale, aggiungi un'altra riga normale in `catalog_items` + +Questo è proprio il vantaggio della separazione: lo stesso `item_id` può comparire sia nel catalogo normale sia nel BC, ma con comportamenti diversi. + +## Differenza pratica tra catalogo normale e BC + +### Catalogo normale + +- gli offer arrivano da `catalog_items` +- hanno costi normali (`cost_credits`, `cost_points`, ecc.) +- quando comprati diventano proprietà utente / inventario + +### Builders Club + +- gli offer arrivano da `catalog_items_bc` +- non hanno prezzo perché il piazzamento BC usa il flow dedicato +- non entrano nell'inventario utente +- non diventano mai proprietà utente +- quando rimossi dalla stanza vengono eliminati + +## Migration da applicare + +Assicurati di avere applicato: + +- `Database Updates/009_add_builders_club_catalog_offers.sql` +- `Database Updates/010_add_catalog_mode_to_catalog_pages.sql` +- `Database Updates/011_add_builders_club_trial_room_lock.sql` +- `Database Updates/012_support_builders_club_catalog_tables.sql` + +La `012` è importante perché aggiorna `catalog_pages_bc.page_layout` con i layout BC moderni: + +- `builders_club_frontpage` +- `builders_club_addons` +- `builders_club_loyalty` + +## Come aggiungere pagine BC + +Le pagine BC vanno create in `catalog_pages_bc`. + +Esempio: + +```sql +INSERT INTO catalog_pages_bc +( + parent_id, + caption, + page_layout, + icon_color, + icon_image, + order_num, + visible, + enabled, + page_headline, + page_teaser, + page_special, + page_text1, + page_text2, + page_text_details, + page_text_teaser +) +VALUES +( + -1, + 'Builders Furni', + 'default_3x3', + 1, + 28, + 1, + '1', + '1', + 'catalog_header_roombuilder', + '', + '', + 'Builders Club', + 'Linea test', + 'Pagina test del Builders Club', + '' +); +``` + +## Come aggiungere furni BC + +I furni BC vanno in `catalog_items_bc`. + +Esempio: + +```sql +INSERT INTO catalog_items_bc +( + item_ids, + page_id, + catalog_name, + order_number, + extradata +) +VALUES +( + '12345', + 1, + 'bc_test_sofa', + 1, + '' +); +``` + +Dove: + +- `item_ids` = ID del base item +- `page_id` = ID pagina in `catalog_pages_bc` +- `catalog_name` = chiave offer/localization + +## Come vendere lo stesso furni anche nel catalogo normale + +Se vuoi che lo stesso furni sia: + +- vendibile nel catalogo normale +- disponibile anche nel Builders Club + +devi avere **due righe distinte**: + +### Normale + +```sql +INSERT INTO catalog_items +( + page_id, + item_ids, + catalog_name, + cost_credits, + cost_points, + points_type, + amount, + club_only, + extradata, + have_offer, + offer_id, + limited_stack, + order_number +) +VALUES +( + 500, + '12345', + 'normal_test_sofa', + 5, + 0, + 0, + 1, + '0', + '', + '1', + -1, + 0, + 1 +); +``` + +### Builders Club + +```sql +INSERT INTO catalog_items_bc +( + item_ids, + page_id, + catalog_name, + order_number, + extradata +) +VALUES +( + '12345', + 1, + 'bc_test_sofa', + 1, + '' +); +``` + +Quindi lo stesso base item `12345` può vivere in entrambi i cataloghi senza condividere il prezzo. + +## Abbonamento e add-on BC + +Abbonamento e add-on non stanno in `catalog_items_bc`. + +Vanno in: + +- `catalog_club_offers` + +Tipi supportati: + +- `BUILDERS_CLUB` +- `BUILDERS_CLUB_ADDON` + +Sono venduti nel catalogo normale, come HC/VIP, ma il widget BC usa comunque le sue pagine dedicate da `catalog_pages_bc`. + +## Nota su `catalog_mode` + +`catalog_mode` resta nella tabella `catalog_pages`, ma non è più il meccanismo principale per far comparire le pagine nel Builders Club. + +Adesso il runtime BC legge direttamente: + +- `catalog_pages_bc` +- `catalog_items_bc` + +Quindi: + +- aggiungere pagine BC in `catalog_pages` non basta +- aggiungere items BC in `catalog_items` non basta +- usare `BOTH` su una pagina normale non la renderà automaticamente una pagina BC + +## Query utili per test + +### Elencare pagine BC + +```sql +SELECT * FROM catalog_pages_bc ORDER BY parent_id, order_num, id; +``` + +### Elencare items BC + +```sql +SELECT * FROM catalog_items_bc ORDER BY page_id, order_number, id; +``` + +### Trovare lo stesso furni in entrambi i cataloghi + +```sql +SELECT 'NORMAL' AS source, id, page_id, item_ids, catalog_name +FROM catalog_items +WHERE item_ids = '12345' + +UNION ALL + +SELECT 'BC' AS source, id, page_id, item_ids, catalog_name +FROM catalog_items_bc +WHERE item_ids = '12345'; +``` + +## Consiglio pratico + +Per fare test rapidi: + +1. crea una pagina in `catalog_pages_bc` +2. inserisci 1-2 furni in `catalog_items_bc` +3. lascia gli stessi furni anche in `catalog_items` se li vuoi vendibili normalmente +4. pubblica / ricarica il catalogo + +Se vuoi, possiamo aggiungere anche un file SQL separato con qualche pagina BC e qualche furni BC già pronti da importare per i test. + +## Seed demo già pronto + +Se vuoi una demo immediata, puoi usare: + +- `Database Updates/013_seed_builders_club_sample_page.sql` + +Questo seed: + +- crea una root BC demo +- crea una pagina BC demo figlia +- duplica alcuni furni già esistenti del catalogo normale dentro `catalog_items_bc` + +Così puoi testare subito il caso: + +- stesso furni vendibile nel catalogo normale +- stesso furni disponibile anche nel Builders Club From cf90ab2bf0df70d3229636cb85553377a6a8357b Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 7 Apr 2026 16:56:57 +0200 Subject: [PATCH 6/8] Fix builders club virtual owner persistence --- .../habbohotel/rooms/BuildersClubRoomSupport.java | 4 ++-- .../com/eu/habbo/habbohotel/rooms/RoomItemManager.java | 2 +- .../java/com/eu/habbo/habbohotel/users/HabboItem.java | 10 +++++++++- .../catalog/BuildersClubPlaceRoomItemEvent.java | 4 +++- .../catalog/BuildersClubPlaceWallItemEvent.java | 4 +++- .../incoming/rooms/items/RoomPlaceItemEvent.java | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java index 9d2ac676..c0227618 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java @@ -27,8 +27,8 @@ public class BuildersClubRoomSupport { private static final Logger LOGGER = LoggerFactory.getLogger(BuildersClubRoomSupport.class); public static final int DEFAULT_TRIAL_FURNI_LIMIT = 50; - // Uses the built-in system account row so Builders Club furni have a valid foreign-key owner in `items`, - // while still being treated as virtual / non-user-owned everywhere else in the BC flow. + // Runtime-only owner marker used to display Builders Club furni as virtual/non-user-owned in-room. + // The actual DB owner for persistence/FK purposes is tracked separately on the item instance. public static final int VIRTUAL_OWNER_ID = 1; public static final String DISPLAY_OWNER_NAME = "Builders Club"; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index e6f991f1..bc330fe1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -628,7 +628,7 @@ public class RoomItemManager { } if (BuildersClubRoomSupport.isTrackedItem(item.getId()) && item.getUserId() != BuildersClubRoomSupport.VIRTUAL_OWNER_ID) { - item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); + item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); item.needsUpdate(true); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java index 9a8849fd..ec9ed53e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java @@ -45,6 +45,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { private int id; private int userId; + private int databaseUserId; private int roomId; private Item baseItem; private String wallPosition; @@ -62,6 +63,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { public HabboItem(ResultSet set, Item baseItem) throws SQLException { this.id = set.getInt("id"); this.userId = set.getInt("user_id"); + this.databaseUserId = this.userId; this.roomId = set.getInt("room_id"); this.baseItem = baseItem; this.wallPosition = set.getString("wall_pos"); @@ -81,6 +83,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { public HabboItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { this.id = id; this.userId = userId; + this.databaseUserId = userId; this.roomId = 0; this.baseItem = item; this.wallPosition = ""; @@ -169,6 +172,11 @@ public abstract class HabboItem implements Runnable, IEventTriggers { public void setUserId(int userId) { this.userId = userId; + this.databaseUserId = userId; + } + + public void setVirtualUserId(int userId) { + this.userId = userId; } public int getRoomId() { @@ -275,7 +283,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { } } else if (this.needsUpdate) { try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) { - statement.setInt(1, this.userId); + statement.setInt(1, this.databaseUserId); statement.setInt(2, this.roomId); statement.setString(3, this.wallPosition); statement.setInt(4, this.x); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java index 622677c3..4684147b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java @@ -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)) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java index 96b29a7c..6c2822cf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java @@ -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)) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java index 7c51b179..1b23813b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java @@ -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); From 17e316e521cd134912dd08128c8e8c62bca9eda8 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 8 Apr 2026 16:18:16 +0200 Subject: [PATCH 7/8] Add wired signal and variable fixes --- .../update_all_interaction_types_wired.sql | 3 + .../com/eu/habbo/habbohotel/items/Item.java | 18 +- .../habbo/habbohotel/items/ItemManager.java | 3 + .../WiredEffectNegativeSendSignal.java | 31 ++ .../WiredEffectNegativeTriggerStacks.java | 30 ++ .../wired/effects/WiredEffectSendSignal.java | 61 +++- .../effects/WiredEffectTriggerStacks.java | 55 +-- .../WiredExtraVariableTextConnector.java | 33 +- .../extra/WiredVariableNameValidator.java | 9 +- .../extra/WiredVariableReferenceSupport.java | 5 +- .../selector/WiredEffectFurniByType.java | 25 +- .../WiredEffectFurniNeighborhood.java | 66 +++- .../WiredEffectUsersNeighborhood.java | 66 +++- .../triggers/WiredTriggerReceiveSignal.java | 46 +++ .../rooms/RoomFurniVariableManager.java | 36 +- .../habbohotel/rooms/RoomItemManager.java | 16 +- .../habbohotel/rooms/RoomSpecialTypes.java | 62 +++- .../rooms/RoomUserVariableManager.java | 49 ++- .../habbohotel/rooms/RoomVariableManager.java | 41 ++- .../habbohotel/wired/WiredEffectType.java | 4 +- .../wired/core/RoomWiredStackIndex.java | 28 ++ .../core/WiredContextVariableSupport.java | 4 +- .../habbohotel/wired/core/WiredEngine.java | 322 +++++++++++++++--- .../habbohotel/wired/core/WiredManager.java | 115 ++++++- .../wired/core/WiredMoveCarryHelper.java | 4 +- .../wired/core/WiredSourceUtil.java | 144 +++++--- .../WiredVariableTextConnectorSupport.java | 59 +++- 27 files changed, 1098 insertions(+), 237 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeSendSignal.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeTriggerStacks.java diff --git a/Database Updates/Items_Base/update_all_interaction_types_wired.sql b/Database Updates/Items_Base/update_all_interaction_types_wired.sql index 1d57887f..f8053225 100644 --- a/Database Updates/Items_Base/update_all_interaction_types_wired.sql +++ b/Database Updates/Items_Base/update_all_interaction_types_wired.sql @@ -2,6 +2,8 @@ UPDATE `items_base` SET `interaction_type` = 'wf_cnd_time_more_than' WHERE `publ 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` = 'wf_act_neg_call_stack' WHERE `public_name` = 'wf_act_neg_call_stack'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_neg_call_stacks' WHERE `public_name` = 'wf_act_neg_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'; @@ -111,6 +113,7 @@ UPDATE `items_base` SET `interaction_type` = 'wf_act_furni_to_furni' WHERE `publ 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_neg_send_signal' WHERE `public_name` = 'wf_act_neg_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'; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java index 4e0a69fe..0ca0afbe 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java @@ -91,7 +91,23 @@ public class Item implements ISerialize { this.allowGift = set.getBoolean("allow_gift"); this.allowInventoryStack = set.getBoolean("allow_inventory_stack"); - this.interactionType = Emulator.getGameEnvironment().getItemManager().getItemInteraction(set.getString("interaction_type").toLowerCase()); + String interactionTypeName = set.getString("interaction_type"); + if (interactionTypeName == null) { + interactionTypeName = "default"; + } + + this.interactionType = Emulator.getGameEnvironment().getItemManager().getItemInteraction(interactionTypeName.toLowerCase()); + + if ((this.interactionType != null) + && "default".equalsIgnoreCase(this.interactionType.getName()) + && (this.fullName != null) + && this.fullName.toLowerCase().startsWith("wf_")) { + ItemInteraction fallbackInteraction = Emulator.getGameEnvironment().getItemManager().getItemInteraction(this.fullName.toLowerCase()); + + if ((fallbackInteraction != null) && !"default".equalsIgnoreCase(fallbackInteraction.getName())) { + this.interactionType = fallbackInteraction; + } + } this.stateCount = set.getShort("interaction_modes_count"); this.effectM = set.getShort("effect_id_male"); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java index 2951ca56..0fc8a660 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java @@ -281,6 +281,8 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_act_move_furni_to", WiredEffectMoveFurniTo.class)); this.interactionsList.add(new ItemInteraction("wf_act_give_reward", WiredEffectGiveReward.class)); this.interactionsList.add(new ItemInteraction("wf_act_call_stacks", WiredEffectTriggerStacks.class)); + this.interactionsList.add(new ItemInteraction("wf_act_neg_call_stack", WiredEffectNegativeTriggerStacks.class)); + this.interactionsList.add(new ItemInteraction("wf_act_neg_call_stacks", WiredEffectNegativeTriggerStacks.class)); this.interactionsList.add(new ItemInteraction("wf_act_kick_user", WiredEffectKickHabbo.class)); this.interactionsList.add(new ItemInteraction("wf_act_mute_triggerer", WiredEffectMuteHabbo.class)); this.interactionsList.add(new ItemInteraction("wf_act_bot_teleport", WiredEffectBotTeleport.class)); @@ -324,6 +326,7 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_slc_furni_with_var", WiredEffectFurniWithVariable.class)); this.interactionsList.add(new ItemInteraction("wf_slc_users_with_var", WiredEffectUsersWithVariable.class)); this.interactionsList.add(new ItemInteraction("wf_act_send_signal", WiredEffectSendSignal.class)); + this.interactionsList.add(new ItemInteraction("wf_act_neg_send_signal", WiredEffectNegativeSendSignal.class)); this.interactionsList.add(new ItemInteraction("wf_act_give_var", WiredEffectGiveVariable.class)); this.interactionsList.add(new ItemInteraction("wf_act_remove_var", WiredEffectRemoveVariable.class)); this.interactionsList.add(new ItemInteraction("wf_act_change_var_val", WiredEffectChangeVariableValue.class)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeSendSignal.java new file mode 100644 index 00000000..14c54913 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeSendSignal.java @@ -0,0 +1,31 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectNegativeSendSignal extends WiredEffectSendSignal { + public static final WiredEffectType type = WiredEffectType.NEG_SEND_SIGNAL; + + public WiredEffectNegativeSendSignal(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectNegativeSendSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected boolean dispatchSignalEvent(WiredEvent event) { + return WiredManager.dispatchEffectTriggeredEvent(event); + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeTriggerStacks.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeTriggerStacks.java new file mode 100644 index 00000000..ae46653b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeTriggerStacks.java @@ -0,0 +1,30 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectNegativeTriggerStacks extends WiredEffectTriggerStacks { + public static final WiredEffectType type = WiredEffectType.NEG_CALL_STACKS; + + public WiredEffectNegativeTriggerStacks(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectNegativeTriggerStacks(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + super.execute(ctx); + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index f57ebd92..460b6936 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -103,34 +103,30 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } LOGGER.debug("[SendSignal] Resolved {} antenna(s), firing signals", resolvedAntennas.size()); - List forwardedUsers = WiredSourceUtil.resolveUsers(ctx, this.userForward); - List forwardedFurni = WiredSourceUtil.resolveItems(ctx, this.furniForward, this.forwardItems); + List forwardedUsers = WiredSourceUtil.resolveUsersRaw(ctx, this.userForward); + List forwardedFurni = WiredSourceUtil.resolveItemsRaw(ctx, this.furniForward, this.forwardItems); RoomUnit defaultUser = forwardedUsers.isEmpty() ? null : forwardedUsers.get(0); - HabboItem defaultFurni = forwardedFurni.isEmpty() ? null : forwardedFurni.get(0); - Collection usersToSend = (signalPerUser && !forwardedUsers.isEmpty()) ? forwardedUsers : Collections.singletonList(defaultUser); - Collection furniToSend = (signalPerFurni && !forwardedFurni.isEmpty()) + Collection furniToSend = !forwardedFurni.isEmpty() ? forwardedFurni - : Collections.singletonList(defaultFurni); + : Collections.singletonList(null); int nextDepth = currentDepth + 1; - boolean isolateBranchContext = (signalPerUser && forwardedUsers.size() > 1) - || (signalPerFurni && forwardedFurni.size() > 1); for (RoomUnit user : usersToSend) { for (HabboItem sourceItem : furniToSend) { for (HabboItem antenna : resolvedAntennas) { - fireSignalAtAntenna(ctx, room, antenna, user, sourceItem, nextDepth, isolateBranchContext); + fireSignalAtAntenna(ctx, room, antenna, user, sourceItem, nextDepth); } } } } - private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth, boolean isolateBranchContext) { + private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth) { if (antenna == null) return; RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); if (tile == null) return; @@ -148,13 +144,13 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .signalChannel(signalChannel) .signalUserCount(actor != null ? 1 : 0) .signalFurniCount(sourceItem != null ? 1 : 0) - .contextVariableScope(isolateBranchContext ? ctx.contextVariables().copy() : ctx.contextVariables()) + .contextVariableScope(ctx.contextVariables()) .triggeredByEffect(true); if (actor != null) builder.actor(actor); if (sourceItem != null) builder.sourceItem(sourceItem); - boolean result = WiredManager.dispatchEffectTriggeredEvent(builder.build()); + boolean result = dispatchSignalEvent(builder.build()); LOGGER.debug("[SendSignal] handleEvent returned: {}", result); } @@ -452,11 +448,52 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { return false; } + public boolean unlinkAntenna(int antennaItemId) { + if (antennaItemId <= 0) { + return false; + } + + boolean changed = false; + + Iterator iterator = this.items.iterator(); + while (iterator.hasNext()) { + HabboItem item = iterator.next(); + + if (item == null || item.getId() != antennaItemId) { + continue; + } + + iterator.remove(); + changed = true; + } + + if (this.antennaSource == antennaItemId) { + if (!this.items.isEmpty()) { + HabboItem firstItem = this.items.iterator().next(); + this.antennaSource = (firstItem != null) ? firstItem.getId() : ANTENNA_PICKED; + } else { + this.antennaSource = ANTENNA_PICKED; + } + + changed = true; + } + + if (changed) { + this.needsUpdate(true); + } + + return changed; + } + @Override protected long requiredCooldown() { return COOLDOWN_TRIGGER_STACKS; } + protected boolean dispatchSignalEvent(WiredEvent event) { + return WiredManager.dispatchEffectTriggeredEvent(event); + } + static class JsonData { int delay; List itemIds; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java index 7c07f009..0adf1de5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java @@ -28,8 +28,8 @@ import java.util.stream.Collectors; public class WiredEffectTriggerStacks extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.CALL_STACKS; - private THashSet items; - private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected THashSet items; + protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredEffectTriggerStacks(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -132,7 +132,7 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect { /** * Maximum recursion depth to prevent infinite loops when trigger stacks call each other. */ - private static final int MAX_STACK_DEPTH = 10; + protected static final int MAX_STACK_DEPTH = 10; @Override public void execute(WiredContext ctx) { @@ -147,30 +147,8 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect { return; } - List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + THashSet usedTiles = collectTargetTiles(room, ctx); - THashSet usedTiles = new THashSet<>(); - - for (HabboItem item : effectiveItems) { - if (item == null) continue; - - boolean found = false; - for (RoomTile tile : usedTiles) { - if (tile.x == item.getX() && tile.y == item.getY()) { - found = true; - break; - } - } - - if (!found) { - RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); - if (tile != null) { - usedTiles.add(tile); - } - } - } - - // Execute effects at tiles with incremented call stack depth WiredManager.executeEffectsAtTiles(usedTiles, roomUnit, room, currentDepth + 1); } @@ -246,6 +224,31 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect { return COOLDOWN_TRIGGER_STACKS; } + protected List resolveEffectiveItems(WiredContext ctx) { + return WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + } + + protected THashSet collectTargetTiles(Room room, WiredContext ctx) { + THashSet usedTiles = new THashSet<>(); + + if (room == null || room.getLayout() == null) { + return usedTiles; + } + + for (HabboItem item : resolveEffectiveItems(ctx)) { + if (item == null) { + continue; + } + + RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); + if (tile != null) { + usedTiles.add(tile); + } + } + + return usedTiles; + } + static class JsonData { int delay; List itemIds; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java index 388f30a4..96149a5d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java @@ -10,6 +10,7 @@ import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; import java.sql.ResultSet; import java.sql.SQLException; @@ -19,7 +20,8 @@ import java.util.Map; public class WiredExtraVariableTextConnector extends InteractionWiredExtra { public static final int CODE = 79; - private static final int MAX_MAPPING_LENGTH = 4096; + public static final int MAX_MAPPING_LENGTH = 1000; + public static final int MAX_MAPPING_LINES = 30; private String mappingsText = ""; private LinkedHashMap mappings = new LinkedHashMap<>(); @@ -38,8 +40,10 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { } @Override - public boolean saveData(WiredSettings settings, GameClient gameClient) { - this.setMappingsText(settings.getStringParam()); + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + String mappingsText = normalizeMappingsText(settings.getStringParam()); + validateMappingsText(mappingsText); + this.setMappingsText(mappingsText); Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); if (room != null) { @@ -156,13 +160,28 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { return ""; } - String normalized = value.replace("\r", ""); + return value.replace("\r", ""); + } - if (normalized.length() > MAX_MAPPING_LENGTH) { - normalized = normalized.substring(0, MAX_MAPPING_LENGTH); + private static void validateMappingsText(String value) throws WiredSaveException { + if (value == null || value.isEmpty()) { + return; } - return normalized; + if (value.length() > MAX_MAPPING_LENGTH) { + throw new WiredSaveException("Variable text connector can contain at most 1000 characters."); + } + + int lineCount = 1; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) == '\n') { + lineCount++; + } + } + + if (lineCount > MAX_MAPPING_LINES) { + throw new WiredSaveException("Variable text connector can contain at most 30 lines."); + } } private static LinkedHashMap parseMappings(String value) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java index 0efcea57..5b3ed4ad 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java @@ -20,21 +20,22 @@ final class WiredVariableNameValidator { return ""; } - return value.trim() + return value .replace("\t", "") .replace("\r", "") - .replace("\n", ""); + .replace("\n", "") + .replaceAll("\\s+", "_"); } static String normalizeLegacy(String value) { String normalized = normalizeForSave(value); if (normalized.contains("=")) { - normalized = normalized.substring(0, normalized.indexOf('=')).trim(); + normalized = normalized.substring(0, normalized.indexOf('=')); } while (normalized.startsWith("@") || normalized.startsWith("~")) { - normalized = normalized.substring(1).trim(); + normalized = normalized.substring(1); } if (normalized.length() > MAX_NAME_LENGTH) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java index 0d2641e7..c83819b2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java @@ -169,11 +169,12 @@ public final class WiredVariableReferenceSupport { } int now = Emulator.getIntUnixTimestamp(); - SharedUserAssignment nextAssignment = (existingAssignment == null) + boolean overwritten = existingAssignment != null && overrideExisting; + SharedUserAssignment nextAssignment = (existingAssignment == null || overwritten) ? new SharedUserAssignment(normalizedValue, now, now) : new SharedUserAssignment(normalizedValue, existingAssignment.getCreatedAt(), Objects.equals(existingAssignment.getValue(), normalizedValue) ? existingAssignment.getUpdatedAt() : now); - if (existingAssignment != null && Objects.equals(existingAssignment.getValue(), normalizedValue)) { + if (!overwritten && existingAssignment != null && Objects.equals(existingAssignment.getValue(), normalizedValue)) { return false; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java index c036239a..02c2630d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java @@ -11,6 +11,7 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; @@ -51,7 +52,6 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { boolean includeWiredItems = this.includeWiredTargets(ctx); List sourceFurni = resolveSourceFurni(ctx, room); - if (sourceFurni.isEmpty()) return; Set matchKeys = new LinkedHashSet<>(); for (HabboItem src : sourceFurni) { @@ -85,12 +85,10 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { .collect(Collectors.toList()); } case SOURCE_FURNI_SIGNAL: { - return new ArrayList<>(ctx.targets().items()); + return WiredSourceUtil.resolveItemsRaw(ctx, WiredSourceUtil.SOURCE_SIGNAL, null); } case SOURCE_FURNI_TRIGGER: { - return ctx.sourceItem() - .map(Collections::singletonList) - .orElse(Collections.emptyList()); + return WiredSourceUtil.resolveItemsRaw(ctx, WiredSourceUtil.SOURCE_TRIGGER, null); } default: return Collections.emptyList(); @@ -104,7 +102,7 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { throw new WiredSaveException("wf_slc_furni_bytype: intParams must have at least 4 elements"); } - this.sourceType = SOURCE_FURNI_PICKED; + this.sourceType = normalizeSourceType(params[0]); this.matchState = params.length > 1 && params[1] == 1; this.filterExisting = params.length > 2 && params[2] == 1; this.invert = params.length > 3 && params[3] == 1; @@ -138,7 +136,7 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { message.appendString(""); message.appendInt(4); - message.appendInt(SOURCE_FURNI_PICKED); + message.appendInt(this.sourceType); message.appendInt(matchState ? 1 : 0); message.appendInt(filterExisting ? 1 : 0); message.appendInt(invert ? 1 : 0); @@ -168,7 +166,7 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { String wiredData = set.getString("wired_data"); if (wiredData != null && wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.sourceType = data.sourceType; + this.sourceType = normalizeSourceType(data.sourceType); this.matchState = data.matchState; this.filterExisting = data.filterExisting; this.invert = data.invert; @@ -190,6 +188,17 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { @Override public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { return false; } + private int normalizeSourceType(int value) { + switch (value) { + case SOURCE_FURNI_SIGNAL: + case SOURCE_FURNI_TRIGGER: + case SOURCE_FURNI_PICKED: + return value; + default: + return SOURCE_FURNI_PICKED; + } + } + static class JsonData { int sourceType; boolean matchState; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java index e1974f6a..8ee8a944 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java @@ -101,24 +101,46 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { } private List resolveSourcePositions(WiredContext ctx, Room room) { - - if (isUserGroup(sourceType)) { - // Prefer the event tile for user-based sources because during walk-on/walk-off - // events the user's position (getX/getY) hasn't been updated yet (stale position). - // The event tile correctly represents where the triggering action occurred. - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); - } - List positions = ctx.targets().users().stream() - .map(u -> new int[]{ u.getX(), u.getY() }) - .collect(Collectors.toList()); - if (positions.isEmpty()) { - ctx.actor().ifPresent(a -> positions.add(new int[]{ a.getX(), a.getY() })); - } - return positions; - } - switch (sourceType) { + case SOURCE_USER_TRIGGER: { + if (ctx.tile().isPresent()) { + return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_SIGNAL: { + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_CLICKED: { + if (ctx.event().getTargetUnit().isPresent()) { + RoomUnit targetUnit = ctx.event().getTargetUnit().get(); + + return Collections.singletonList(new int[]{ targetUnit.getX(), targetUnit.getY() }); + } + + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return Collections.emptyList(); + } case SOURCE_FURNI_TRIGGER: { return ctx.sourceItem() .map(i -> Collections.singletonList(new int[]{ i.getX(), i.getY() })) @@ -132,9 +154,17 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { .collect(Collectors.toList()); } case SOURCE_FURNI_SIGNAL: { - return ctx.targets().items().stream() + List positions = ctx.targets().items().stream() .map(i -> new int[]{ i.getX(), i.getY() }) .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.sourceItem() + .map(item -> Collections.singletonList(new int[]{ item.getX(), item.getY() })) + .orElse(Collections.emptyList()); } default: return Collections.emptyList(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java index 9c5dd91c..db19bea6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java @@ -111,24 +111,46 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { } private List resolveSourcePositions(WiredContext ctx, Room room) { - - if (isUserGroup(sourceType)) { - // Prefer the event tile for user-based sources because during walk-on/walk-off - // events the user's position (getX/getY) hasn't been updated yet (stale position). - // The event tile correctly represents where the triggering action occurred. - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); - } - List positions = ctx.targets().users().stream() - .map(u -> new int[]{ u.getX(), u.getY() }) - .collect(Collectors.toList()); - if (positions.isEmpty()) { - ctx.actor().ifPresent(a -> positions.add(new int[]{ a.getX(), a.getY() })); - } - return positions; - } - switch (sourceType) { + case SOURCE_USER_TRIGGER: { + if (ctx.tile().isPresent()) { + return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_SIGNAL: { + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_CLICKED: { + if (ctx.event().getTargetUnit().isPresent()) { + RoomUnit targetUnit = ctx.event().getTargetUnit().get(); + + return Collections.singletonList(new int[]{ targetUnit.getX(), targetUnit.getY() }); + } + + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return Collections.emptyList(); + } case SOURCE_FURNI_TRIGGER: { return ctx.sourceItem() .map(i -> Collections.singletonList(new int[]{ i.getX(), i.getY() })) @@ -142,9 +164,17 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { .collect(Collectors.toList()); } case SOURCE_FURNI_SIGNAL: { - return ctx.targets().items().stream() + List positions = ctx.targets().items().stream() .map(i -> new int[]{ i.getX(), i.getY() }) .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.sourceItem() + .map(item -> Collections.singletonList(new int[]{ item.getX(), item.getY() })) + .orElse(Collections.emptyList()); } default: return Collections.emptyList(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java index 77bf5f29..391293ba 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java @@ -68,6 +68,52 @@ public class WiredTriggerReceiveSignal extends InteractionWiredTrigger { return channel; } + public boolean unlinkAntenna(int antennaItemId) { + if (antennaItemId <= 0) { + return false; + } + + boolean changed = false; + + if (!this.items.isEmpty()) { + THashSet itemsToRemove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (item == null || item.getId() == antennaItemId) { + itemsToRemove.add(item); + } + } + + if (!itemsToRemove.isEmpty()) { + this.items.removeAll(itemsToRemove); + changed = true; + } + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + int nextChannel = 0; + + if (!this.items.isEmpty()) { + HabboItem firstItem = this.items.iterator().next(); + nextChannel = (firstItem != null) ? firstItem.getId() : 0; + } + + if (this.channel != nextChannel) { + this.channel = nextChannel; + changed = true; + } + } else if (this.channel == antennaItemId) { + this.channel = 0; + changed = true; + } + + if (changed) { + this.needsUpdate(true); + } + + return changed; + } + @Override public boolean isTriggeredByRoomUnit() { return false; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java index 3f040ceb..0a199b43 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java @@ -147,12 +147,14 @@ public class RoomFurniVariableManager { return false; } - boolean changed = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + boolean overwritten = existingAssignment != null && overrideExisting; + boolean valueChanged = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + boolean changed = overwritten || valueChanged; - if (existingAssignment == null) { + if (existingAssignment == null || overwritten) { int now = Emulator.getIntUnixTimestamp(); assignments.put(definitionItemId, new VariableAssignment(normalizedValue, now, now)); - } else if (changed) { + } else if (valueChanged) { existingAssignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); } @@ -613,7 +615,13 @@ public class RoomFurniVariableManager { for (InteractionWiredExtra extra : extras) { if (extra instanceof WiredExtraFurniVariable) { - result.add((WiredExtraFurniVariable) extra); + WiredExtraFurniVariable definition = (WiredExtraFurniVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + continue; + } + + result.add(definition); } } @@ -659,6 +667,11 @@ public class RoomFurniVariableManager { if (extra instanceof WiredExtraFurniVariable) { WiredExtraFurniVariable definition = (WiredExtraFurniVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + return null; + } + return new WiredVariableDefinitionInfo( definition.getId(), definition.getVariableName(), @@ -670,7 +683,8 @@ public class RoomFurniVariableManager { } if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isFurniEcho()) { - return ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + WiredVariableDefinitionInfo info = ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + return (info != null && hasVisibleDefinitionName(info.getName())) ? info : null; } return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); @@ -703,7 +717,13 @@ public class RoomFurniVariableManager { for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isFurniEcho()) { - result.add((WiredExtraVariableEcho) extra); + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + + if (!hasVisibleDefinitionName(echo.getVariableName())) { + continue; + } + + result.add(echo); } } @@ -711,6 +731,10 @@ public class RoomFurniVariableManager { return result; } + private static boolean hasVisibleDefinitionName(String name) { + return name != null && !name.trim().isEmpty(); + } + private VariableAssignment getRawAssignment(int furniId, int definitionItemId) { if (furniId <= 0 || definitionItemId <= 0) { return null; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index bc330fe1..a40bdbf6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -804,6 +804,11 @@ public class RoomItemManager { return; } + boolean cleanedSignalAntennaReferences = false; + if (this.isAntennaItem(item)) { + cleanedSignalAntennaReferences = specialTypes.unlinkSignalAntennaReferences(item.getId()); + } + this.room.getFurniVariableManager().removeAssignmentsForFurni(item.getId()); boolean isWiredItem = false; @@ -905,11 +910,20 @@ public class RoomItemManager { } // Invalidate wired cache when wired items are removed - if (isWiredItem) { + if (isWiredItem || cleanedSignalAntennaReferences) { WiredManager.invalidateRoom(this.room); } } + private boolean isAntennaItem(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionType = item.getBaseItem().getInteractionType().getName(); + return interactionType != null && interactionType.equalsIgnoreCase("antenna"); + } + // ==================== ITEM UPDATES ==================== /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java index 9f6e1b5e..dcbdfd11 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java @@ -348,7 +348,7 @@ public class RoomSpecialTypes { public static final int MAX_SENDERS_PER_RECEIVER = 5; public boolean isSignalSenderLimitReached() { - Set existing = this.wiredEffects.get(WiredEffectType.SEND_SIGNAL); + Set existing = this.getSignalSenders(); return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM; } @@ -358,7 +358,7 @@ public class RoomSpecialTypes { } public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) { - Set senders = this.wiredEffects.get(WiredEffectType.SEND_SIGNAL); + Set senders = this.getSignalSenders(); if (senders == null) return 0; int count = 0; @@ -383,7 +383,7 @@ public class RoomSpecialTypes { return 0; } - Set senders = this.wiredEffects.get(WiredEffectType.SEND_SIGNAL); + Set senders = this.getSignalSenders(); if (senders == null) { return 0; } @@ -411,6 +411,62 @@ public class RoomSpecialTypes { return countSendersTargetingAnyReceiver(receiverItemIds, null); } + public boolean unlinkSignalAntennaReferences(int antennaItemId) { + if (antennaItemId <= 0) { + return false; + } + + boolean changed = false; + + THashSet receivers = this.getTriggers(WiredTriggerType.RECEIVE_SIGNAL); + for (InteractionWiredTrigger trigger : receivers) { + if (!(trigger instanceof com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal receiver)) { + continue; + } + + if (!receiver.unlinkAntenna(antennaItemId)) { + continue; + } + + changed = true; + Emulator.getThreading().run(receiver); + } + + Set senders = this.getSignalSenders(); + if (senders != null) { + for (InteractionWiredEffect effect : senders) { + if (!(effect instanceof WiredEffectSendSignal sender)) { + continue; + } + + if (!sender.unlinkAntenna(antennaItemId)) { + continue; + } + + changed = true; + Emulator.getThreading().run(sender); + } + } + + return changed; + } + + private Set getSignalSenders() { + Set senders = new HashSet<>(); + + Set standardSenders = this.wiredEffects.get(WiredEffectType.SEND_SIGNAL); + if (standardSenders != null) { + senders.addAll(standardSenders); + } + + Set negativeSenders = this.wiredEffects.get(WiredEffectType.NEG_SEND_SIGNAL); + if (negativeSenders != null) { + senders.addAll(negativeSenders); + } + + return senders.isEmpty() ? null : senders; + } + public void addTrigger(InteractionWiredTrigger trigger) { // Add to type-based index this.wiredTriggers.computeIfAbsent(trigger.getType(), k -> ConcurrentHashMap.newKeySet()) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java index a05df8c6..a38b3a1b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java @@ -158,12 +158,14 @@ public class RoomUserVariableManager { return false; } - boolean changed = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + boolean overwritten = existingAssignment != null && overrideExisting; + boolean valueChanged = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + boolean changed = overwritten || valueChanged; - if (existingAssignment == null) { + if (existingAssignment == null || overwritten) { int now = Emulator.getIntUnixTimestamp(); assignments.put(definitionItemId, new VariableAssignment(normalizedValue, now, now)); - } else if (changed) { + } else if (valueChanged) { existingAssignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); } @@ -686,7 +688,13 @@ public class RoomUserVariableManager { for (InteractionWiredExtra extra : extras) { if (extra instanceof WiredExtraUserVariable) { - result.add((WiredExtraUserVariable) extra); + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + continue; + } + + result.add(definition); } } @@ -743,6 +751,11 @@ public class RoomUserVariableManager { if (extra instanceof WiredExtraUserVariable) { WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + return null; + } + return new WiredVariableDefinitionInfo( definition.getId(), definition.getVariableName(), @@ -755,11 +768,17 @@ public class RoomUserVariableManager { if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + return null; + } + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); } if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isUserEcho()) { - return ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + WiredVariableDefinitionInfo info = ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + return (info != null && hasVisibleDefinitionName(info.getName())) ? info : null; } return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); @@ -792,7 +811,13 @@ public class RoomUserVariableManager { for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { - result.add((WiredExtraVariableReference) extra); + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + continue; + } + + result.add(reference); } } @@ -809,7 +834,13 @@ public class RoomUserVariableManager { for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isUserEcho()) { - result.add((WiredExtraVariableEcho) extra); + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + + if (!hasVisibleDefinitionName(echo.getVariableName())) { + continue; + } + + result.add(echo); } } @@ -817,6 +848,10 @@ public class RoomUserVariableManager { return result; } + private static boolean hasVisibleDefinitionName(String name) { + return name != null && !name.trim().isEmpty(); + } + private VariableAssignment getRawAssignment(int userId, int definitionItemId) { if (userId <= 0 || definitionItemId <= 0) { return null; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java index 25b87649..100b2d23 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java @@ -457,7 +457,13 @@ public class RoomVariableManager { for (InteractionWiredExtra extra : extras) { if (extra instanceof WiredExtraRoomVariable) { - result.add((WiredExtraRoomVariable) extra); + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + continue; + } + + result.add(definition); } } @@ -503,6 +509,11 @@ public class RoomVariableManager { if (extra instanceof WiredExtraRoomVariable) { WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + return null; + } + return new WiredVariableDefinitionInfo( definition.getId(), definition.getVariableName(), @@ -515,11 +526,17 @@ public class RoomVariableManager { if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + return null; + } + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); } if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isRoomEcho()) { - return ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + WiredVariableDefinitionInfo info = ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + return (info != null && hasVisibleDefinitionName(info.getName())) ? info : null; } return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); @@ -557,7 +574,13 @@ public class RoomVariableManager { for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { - result.add((WiredExtraVariableReference) extra); + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + continue; + } + + result.add(reference); } } @@ -574,7 +597,13 @@ public class RoomVariableManager { for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isRoomEcho()) { - result.add((WiredExtraVariableEcho) extra); + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + + if (!hasVisibleDefinitionName(echo.getVariableName())) { + continue; + } + + result.add(echo); } } @@ -696,6 +725,10 @@ public class RoomVariableManager { return 0; } + private static boolean hasVisibleDefinitionName(String name) { + return name != null && !name.trim().isEmpty(); + } + public static class Snapshot { private final int roomId; private final List definitions; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java index 845805a8..e80934c4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java @@ -59,7 +59,9 @@ public enum WiredEffectType { REMOVE_VAR(73), CHANGE_VAR_VAL(74), FURNI_WITH_VAR_SELECTOR(75), - USERS_WITH_VAR_SELECTOR(76); + USERS_WITH_VAR_SELECTOR(76), + NEG_CALL_STACKS(86), + NEG_SEND_SIGNAL(87); public final int code; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java index 5be6f490..42cbddea 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java @@ -158,6 +158,34 @@ public final class RoomWiredStackIndex implements WiredStackIndex { return stacks.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(stacks); } + public List getStacksAtTile(Room room, RoomTile tile) { + if (room == null || tile == null || room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + RoomSpecialTypes specialTypes = room.getRoomSpecialTypes(); + THashSet triggers = specialTypes.getTriggers(tile.x, tile.y); + + if (triggers == null || triggers.isEmpty()) { + return Collections.emptyList(); + } + + List stacks = new ArrayList<>(); + + for (InteractionWiredTrigger trigger : WiredExecutionOrderUtil.sort(triggers)) { + if (trigger == null) { + continue; + } + + WiredStack stack = buildStack(room, specialTypes, trigger, tile.x, tile.y); + if (stack != null) { + stacks.add(stack); + } + } + + return stacks.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(stacks); + } + /** * Build a single wired stack for a trigger at a specific location. */ diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java index 1c2a2961..e1e45791 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java @@ -71,7 +71,7 @@ public final class WiredContextVariableSupport { public static WiredVariableDefinitionInfo getDefinitionInfo(Room room, int definitionItemId) { WiredExtraContextVariable definition = getDefinition(room, definitionItemId); - if (definition == null) { + if (definition == null || definition.getVariableName() == null || definition.getVariableName().trim().isEmpty()) { return null; } @@ -85,7 +85,7 @@ public final class WiredContextVariableSupport { } public static boolean hasDefinition(Room room, int definitionItemId) { - return getDefinition(room, definitionItemId) != null; + return getDefinitionInfo(room, definitionItemId) != null; } public static boolean assignVariable(WiredContext ctx, Room room, int definitionItemId, Integer value, boolean overrideExisting) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index 5a96d607..91f25afe 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -19,6 +19,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredConditionOperator; +import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.api.IWiredCondition; import com.eu.habbo.habbohotel.wired.api.IWiredEffect; import com.eu.habbo.habbohotel.wired.api.WiredStack; @@ -159,6 +160,10 @@ public final class WiredEngine { * @return true if any stack was triggered (useful for SAY_SOMETHING to suppress message) */ public boolean handleEvent(WiredEvent event) { + return handleEvent(event, false); + } + + public boolean handleEvent(WiredEvent event, boolean negateConditions) { if (event == null) { return false; } @@ -192,7 +197,7 @@ public final class WiredEngine { roomRecursionDepth.put(roomId, currentDepth + 1); try { - return handleEventInternal(event, room); + return handleEventInternal(event, room, negateConditions); } finally { // Decrement recursion depth int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1; @@ -329,7 +334,7 @@ public final class WiredEngine { /** * Internal event handling after recursion check. */ - private boolean handleEventInternal(WiredEvent event, Room room) { + private boolean handleEventInternal(WiredEvent event, Room room, boolean negateConditions) { // Find candidate stacks for this event type List stacks = index.getStacks(room, event.getType()); @@ -345,7 +350,7 @@ public final class WiredEngine { for (WiredStack stack : stacks) { try { - boolean triggered = processStack(stack, event, triggerTime); + boolean triggered = processStack(stack, event, triggerTime, negateConditions); if (triggered) { anyTriggered = true; @@ -374,6 +379,10 @@ public final class WiredEngine { * Process a single wired stack. */ private boolean processStack(WiredStack stack, WiredEvent event, long currentTime) { + return processStack(stack, event, currentTime, false); + } + + private boolean processStack(WiredStack stack, WiredEvent event, long currentTime, boolean negateConditions) { Room room = event.getRoom(); WiredTextInputCaptureSupport.CaptureResult captureResult = resolveTextInputCapture(stack, event); @@ -417,17 +426,12 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } - // Evaluate conditions - if (stack.hasConditions()) { - debug(room, "Evaluating {} conditions...", stack.conditions().size()); - boolean conditionsPassed = evaluateConditions(stack, ctx); - debug(room, "Conditions result: {}", conditionsPassed ? "PASSED" : "FAILED"); - if (!conditionsPassed) { - debug(room, "Conditions failed, aborting stack"); - return false; - } - } else { - debug(room, "No conditions in stack, proceeding to effects"); + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); + List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); + boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); + + if (!shouldContinueAfterConditionCheck(stack, room, conditionsPassedForExecution, executableEffects, hasSpecialOutcome)) { + return false; } WiredExtraExecutionLimit executionLimitExtra = getExecutionLimitExtra(room, stack); @@ -455,7 +459,8 @@ public final class WiredEngine { return false; } - if ((event.getType() == WiredEvent.Type.USER_CLICKS_USER) + if (conditionsPassedForExecution + && (event.getType() == WiredEvent.Type.USER_CLICKS_USER) && (stack.triggerItem() instanceof WiredTriggerHabboClicksUser) && event.getActor().isPresent()) { WiredTriggerHabboClicksUser clickUserTrigger = (WiredTriggerHabboClicksUser) stack.triggerItem(); @@ -477,8 +482,8 @@ public final class WiredEngine { finalizeSelectors(executedSelectors, ctx, currentTime); // Execute effects - if (stack.hasEffects()) { - executeEffects(stack, ctx, currentTime); + if (!executableEffects.isEmpty()) { + executeEffects(stack, executableEffects, ctx, currentTime); } // Fire executed event @@ -494,6 +499,139 @@ public final class WiredEngine { return true; } + public boolean executeDirectStack(WiredStack stack, WiredEvent event, boolean negateConditions) { + if (stack == null || event == null) { + return false; + } + + Room room = event.getRoom(); + if (room == null) { + return false; + } + + if (stack.trigger().requiresActor() && !event.getActor().isPresent()) { + return false; + } + + if (!stackHasExecutableOutcome(stack, event)) { + return false; + } + + long currentTime = System.currentTimeMillis(); + + WiredState state = new WiredState(maxStepsPerStack); + WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + WiredRoomDiagnostics diagnostics = getDiagnostics(room.getId()); + + state.step(); + + int stackCost = estimateStackCost(stack, roomRecursionDepth.getOrDefault(room.getId(), 0)); + String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event); + int monitorSourceId = getMonitorSourceId(stack.triggerItem()); + + debug(room, "Direct stack execution for item {} (conditions: {}, effects: {}, negated: {})", + stack.triggerItem() != null ? stack.triggerItem().getId() : "null", + stack.conditions().size(), + stack.effects().size(), + negateConditions); + + List executedSelectors = Collections.emptyList(); + if (stack.hasEffects()) { + executedSelectors = executeSelectors(stack, ctx); + applySelectionFilterExtras(stack, ctx, executedSelectors); + } + + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); + List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); + boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); + + if (!shouldContinueAfterConditionCheck(stack, room, conditionsPassedForExecution, executableEffects, hasSpecialOutcome)) { + return false; + } + + WiredExtraExecutionLimit executionLimitExtra = getExecutionLimitExtra(room, stack); + if (executionLimitExtra != null && !executionLimitExtra.tryAcquireExecutionSlot(currentTime)) { + debug(room, "Execution limit blocked direct stack {} (max {} in {} ms)", + stack.triggerItem() != null ? stack.triggerItem().getId() : "null", + executionLimitExtra.getMaxExecutions(), + executionLimitExtra.getTimeWindowMs()); + return false; + } + + if (!fireTriggeredEvent(stack, event)) { + debug(room, "Direct stack cancelled by plugin"); + return false; + } + + if (!diagnostics.tryConsumeExecutionBudget( + stackCost, + currentTime, + monitorSourceLabel, + monitorSourceId, + buildStackMonitorReason(stack, event, stackCost))) { + debug(room, "Execution cap blocked direct stack {}", stack.triggerItem() != null ? stack.triggerItem().getId() : "null"); + return false; + } + + RoomUnit actor = event.getActor().orElse(null); + + if (stack.triggerItem() instanceof InteractionWiredTrigger) { + InteractionWiredTrigger trigger = (InteractionWiredTrigger) stack.triggerItem(); + trigger.activateBox(room, actor, currentTime); + } + + activateExtras(room, stack.triggerItem(), actor, currentTime); + finalizeSelectors(executedSelectors, ctx, currentTime); + + if (!executableEffects.isEmpty()) { + executeEffects(stack, executableEffects, ctx, currentTime); + } + + fireExecutedEvent(stack, event); + diagnostics.recordExecution( + state.elapsedMs(), + System.currentTimeMillis(), + monitorSourceLabel, + monitorSourceId, + buildExecutionMonitorReason(stack, state.elapsedMs()) + ); + + return true; + } + + public boolean shouldExecuteDirectStack(WiredStack stack, WiredEvent event, boolean negateConditions) { + if (stack == null || event == null) { + return false; + } + + Room room = event.getRoom(); + if (room == null) { + return false; + } + + if (stack.trigger().requiresActor() && !event.getActor().isPresent()) { + return false; + } + + if (!stack.hasEffects()) { + return false; + } + + WiredState state = new WiredState(maxStepsPerStack); + WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + state.step(); + + List executedSelectors = Collections.emptyList(); + if (stack.hasEffects()) { + executedSelectors = executeSelectors(stack, ctx); + applySelectionFilterExtras(stack, ctx, executedSelectors); + } + + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); + List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); + return !executableEffects.isEmpty(); + } + private boolean wouldTriggerStack(WiredStack stack, WiredEvent event, long currentTime) { Room room = event.getRoom(); WiredTextInputCaptureSupport.CaptureResult captureResult = resolveTextInputCapture(stack, event); @@ -522,7 +660,14 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } - if (stack.hasConditions() && !evaluateConditions(stack, ctx)) { + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false); + if (!conditionsPassedForExecution) { + return false; + } + + List executableEffects = getExecutableEffectsForCurrentExecution(stack, true); + boolean hasSpecialOutcome = hasSpecialTriggerOutcome(stack, event); + if (executableEffects.isEmpty() && !hasSpecialOutcome) { return false; } @@ -553,6 +698,67 @@ public final class WiredEngine { return false; } + private boolean hasSpecialTriggerOutcome(WiredStack stack, WiredEvent event) { + if (stack == null) { + return false; + } + + if (stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword) { + return ((WiredTriggerHabboSaysKeyword) stack.triggerItem()).isHideMessage(); + } + + if ((event != null) + && (event.getType() == WiredEvent.Type.USER_CLICKS_USER) + && (stack.triggerItem() instanceof WiredTriggerHabboClicksUser)) { + WiredTriggerHabboClicksUser trigger = (WiredTriggerHabboClicksUser) stack.triggerItem(); + return trigger.isBlockMenuOpen() || trigger.isDoNotRotate(); + } + + return false; + } + + private boolean getConditionOutcomeForExecution(WiredStack stack, WiredContext ctx, boolean negateConditions) { + if (!stack.hasConditions()) { + return !negateConditions; + } + + return shouldConditionsPass(stack, ctx, negateConditions); + } + + private List getExecutableEffectsForCurrentExecution(WiredStack stack, boolean conditionsPassed) { + List executableEffects = new ArrayList<>(); + + for (IWiredEffect effect : stack.effects()) { + if (effect == null || effect.isSelector()) { + continue; + } + + boolean negativeEffect = isNegativeConditionEffect(effect); + + if (conditionsPassed) { + if (!negativeEffect) { + executableEffects.add(effect); + } + continue; + } + + if (stack.hasConditions() && negativeEffect) { + executableEffects.add(effect); + } + } + + return executableEffects; + } + + private boolean isNegativeConditionEffect(IWiredEffect effect) { + if (!(effect instanceof InteractionWiredEffect)) { + return false; + } + + WiredEffectType effectType = ((InteractionWiredEffect) effect).getType(); + return effectType == WiredEffectType.NEG_CALL_STACKS || effectType == WiredEffectType.NEG_SEND_SIGNAL; + } + private WiredTextInputCaptureSupport.CaptureResult resolveTextInputCapture(WiredStack stack, WiredEvent event) { if (stack == null || event == null) { return WiredTextInputCaptureSupport.CaptureResult.noMatch(); @@ -576,6 +782,48 @@ public final class WiredEngine { return evaluateConditionsByMode(conditions, ctx, stack.conditionEvaluationMode(), stack.conditionEvaluationValue()); } + private boolean shouldContinueAfterConditionCheck(WiredStack stack, Room room, boolean conditionsPassedForExecution, List executableEffects, boolean hasSpecialOutcome) { + if (stack.hasConditions()) { + debug(room, "Evaluating {} conditions...", stack.conditions().size()); + + if (!conditionsPassedForExecution && !executableEffects.isEmpty()) { + debug(room, "Conditions failed, executing negative effects"); + return true; + } + + if (!conditionsPassedForExecution) { + debug(room, "Conditions failed, aborting stack"); + return false; + } + + if (hasSpecialOutcome || !executableEffects.isEmpty()) { + return true; + } + + debug(room, "Conditions passed, but no executable effects remain"); + return false; + } + + if (!conditionsPassedForExecution) { + debug(room, "No conditions in stack, negated execution aborted"); + return false; + } + + if (hasSpecialOutcome || !executableEffects.isEmpty()) { + debug(room, "No conditions in stack, proceeding to effects"); + return true; + } + + debug(room, "No conditions in stack, but no executable effects remain"); + return false; + } + + private boolean shouldConditionsPass(WiredStack stack, WiredContext ctx, boolean negateConditions) { + boolean conditionsPassed = evaluateConditions(stack, ctx); + debug(ctx.room(), "Conditions result: {}", conditionsPassed ? "PASSED" : "FAILED"); + return negateConditions ? !conditionsPassed : conditionsPassed; + } + /** * Evaluate conditions according to the configured stack mode. */ @@ -638,65 +886,57 @@ public final class WiredEngine { /** * Execute effects in a stack. */ - private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) { - List effects = stack.effects(); - + private void executeEffects(WiredStack stack, List effects, WiredContext ctx, long currentTime) { if (effects.isEmpty()) { return; } - // Selectors already executed before conditions; only run regular effects here - List regulars = new ArrayList<>(); - for (IWiredEffect e : effects) { - if (!e.isSelector()) regulars.add(e); - } - // Determine which (regular) effects to execute List toExecute; if (stack.useRandom()) { WiredExtraRandom randomExtra = getRandomExtra(ctx.room(), stack); - if (regulars.isEmpty()) { + if (effects.isEmpty()) { toExecute = new ArrayList<>(); } else if (randomExtra != null) { - toExecute = randomExtra.selectWiredEffects(regulars); + toExecute = randomExtra.selectWiredEffects(effects); debug(ctx.room(), "Random mode: selected {} effect(s), skip window {}", toExecute.size(), randomExtra.getSkipExecutions()); } else { - int randomIndex = new Random().nextInt(regulars.size()); - toExecute = Collections.singletonList(regulars.get(randomIndex)); - debug(ctx.room(), "Random mode: selected effect {}/{}", randomIndex + 1, regulars.size()); + int randomIndex = new Random().nextInt(effects.size()); + toExecute = Collections.singletonList(effects.get(randomIndex)); + debug(ctx.room(), "Random mode: selected effect {}/{}", randomIndex + 1, effects.size()); } } else if (stack.useUnseen()) { // Unseen mode: execute in stable order with memory - if (regulars.isEmpty()) { + if (effects.isEmpty()) { toExecute = new ArrayList<>(); } else { WiredExtraUnseen unseenExtra = getUnseenExtra(ctx.room(), stack); if (unseenExtra != null) { - toExecute = unseenExtra.selectWiredEffects(regulars); + toExecute = unseenExtra.selectWiredEffects(effects); if (!toExecute.isEmpty()) { - int selectedIndex = regulars.indexOf(toExecute.get(0)); - debug(ctx.room(), "Unseen mode: selected effect {}/{}", selectedIndex + 1, regulars.size()); + int selectedIndex = effects.indexOf(toExecute.get(0)); + debug(ctx.room(), "Unseen mode: selected effect {}/{}", selectedIndex + 1, effects.size()); } else { debug(ctx.room(), "Unseen mode: no eligible effect found"); } } else { - int index = getNextUnseenIndex(stack, regulars.size()); - toExecute = Collections.singletonList(regulars.get(index)); - debug(ctx.room(), "Unseen mode fallback: selected effect {}/{}", index + 1, regulars.size()); + int index = getNextUnseenIndex(stack, effects.size()); + toExecute = Collections.singletonList(effects.get(index)); + debug(ctx.room(), "Unseen mode fallback: selected effect {}/{}", index + 1, effects.size()); } } } else if (stack.executeInOrder()) { debug(ctx.room(), "Ordered mode: executing effect batches in stack order by delay"); - executeOrderedEffects(regulars, ctx, currentTime); + executeOrderedEffects(effects, ctx, currentTime); return; } else { // Normal mode: preserve the physical stack order. // This matches the legacy handler behavior and avoids visual/state races // for combinations such as Move/Rotate + Match To Snapshot in the same stack. - toExecute = new ArrayList<>(regulars); + toExecute = new ArrayList<>(effects); } WiredMoveCarryHelper.beginMovementCollection(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java index 5a02b34a..085170d6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java @@ -18,6 +18,7 @@ import com.eu.habbo.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredGiveRewardItem; import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.api.WiredStack; import com.eu.habbo.habbohotel.wired.migrate.WiredEvents; import com.eu.habbo.habbohotel.wired.tick.WiredTickService; import com.eu.habbo.habbohotel.wired.tick.WiredTickable; @@ -40,6 +41,10 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Set; /** * Manager class for the wired runtime. @@ -95,7 +100,7 @@ public final class WiredManager { /** Whether the engine is initialized */ private static volatile boolean initialized = false; private static final ThreadLocal EVENT_HANDLING_DEPTH = new ThreadLocal<>(); - private static final ThreadLocal> DEFERRED_EFFECT_EVENTS = new ThreadLocal<>(); + private static final ThreadLocal> DEFERRED_EFFECT_EVENTS = new ThreadLocal<>(); private WiredManager() { // Static utility class } @@ -241,6 +246,10 @@ public final class WiredManager { * @return true if any stack was triggered */ public static boolean handleEvent(WiredEvent event) { + return handleEvent(event, false); + } + + public static boolean handleEvent(WiredEvent event, boolean negateConditions) { if (!isEnabled() || engine == null) { return false; } @@ -260,19 +269,19 @@ public final class WiredManager { boolean handled = false; try { - handled = engine.handleEvent(event); + handled = engine.handleEvent(event, negateConditions); if (nextDepth == 1) { - ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); + ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); while (deferredEvents != null && !deferredEvents.isEmpty()) { - WiredEvent deferredEvent = deferredEvents.pollFirst(); + DeferredEffectEvent deferredEvent = deferredEvents.pollFirst(); - if (deferredEvent == null || RoomWiredDisableSupport.isWiredDisabled(deferredEvent.getRoom())) { + if (deferredEvent == null || deferredEvent.event == null || RoomWiredDisableSupport.isWiredDisabled(deferredEvent.event.getRoom())) { continue; } - handled = engine.handleEvent(deferredEvent) || handled; + handled = engine.handleEvent(deferredEvent.event, deferredEvent.negateConditions) || handled; } } @@ -288,6 +297,14 @@ public final class WiredManager { } public static boolean dispatchEffectTriggeredEvent(WiredEvent event) { + return dispatchEffectTriggeredEvent(event, false); + } + + public static boolean dispatchNegatedEffectTriggeredEvent(WiredEvent event) { + return dispatchEffectTriggeredEvent(event, true); + } + + private static boolean dispatchEffectTriggeredEvent(WiredEvent event, boolean negateConditions) { if (!isEnabled() || engine == null || event == null || RoomWiredDisableSupport.isWiredDisabled(event.getRoom())) { return false; } @@ -295,17 +312,17 @@ public final class WiredManager { Integer currentDepth = EVENT_HANDLING_DEPTH.get(); if (currentDepth == null || currentDepth <= 0) { - return handleEvent(event); + return handleEvent(event, negateConditions); } - ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); + ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); if (deferredEvents == null) { deferredEvents = new ArrayDeque<>(); DEFERRED_EFFECT_EVENTS.set(deferredEvents); } - deferredEvents.addLast(event); + deferredEvents.addLast(new DeferredEffectEvent(event, negateConditions)); return true; } @@ -971,6 +988,10 @@ public final class WiredManager { * @return true if any effects were executed */ public static boolean executeEffectsAtTiles(THashSet tiles, final RoomUnit roomUnit, final Room room, final int callStackDepth) { + if (tiles == null || tiles.isEmpty() || room == null || engine == null || stackIndex == null) { + return false; + } + for (RoomTile tile : tiles) { if (room != null) { THashSet items = room.getItemsAt(tile); @@ -994,6 +1015,82 @@ public final class WiredManager { return true; } + public static boolean executeNegatedStacksAtTiles(THashSet tiles, final RoomUnit roomUnit, final Room room, final int callStackDepth) { + if (tiles == null || tiles.isEmpty() || room == null || engine == null || stackIndex == null) { + return false; + } + + boolean handled = false; + WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room) + .actor(roomUnit) + .callStackDepth(callStackDepth) + .build(); + + for (RoomTile tile : tiles) { + List stacks = stackIndex.getStacksAtTile(room, tile); + if (stacks.isEmpty()) { + continue; + } + + for (WiredStack stack : stacks) { + handled = engine.executeDirectStack(stack, event, true) || handled; + } + } + + return handled; + } + + public static boolean executeNegatedTargetStacks(Iterable triggerItems, final RoomUnit roomUnit, final Room room, final int callStackDepth) { + if (triggerItems == null || room == null || engine == null || stackIndex == null || room.getLayout() == null) { + return false; + } + + boolean handled = false; + Set seenTriggerIds = new HashSet<>(); + WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room) + .actor(roomUnit) + .callStackDepth(callStackDepth) + .build(); + + for (HabboItem triggerItem : triggerItems) { + if (triggerItem == null || !seenTriggerIds.add(triggerItem.getId())) { + continue; + } + + RoomTile tile = room.getLayout().getTile(triggerItem.getX(), triggerItem.getY()); + if (tile == null) { + continue; + } + + List stacks = stackIndex.getStacksAtTile(room, tile); + if (stacks.isEmpty()) { + continue; + } + + for (WiredStack stack : stacks) { + HabboItem stackTriggerItem = stack.triggerItem(); + if (stackTriggerItem == null || stackTriggerItem.getId() != triggerItem.getId()) { + continue; + } + + handled = engine.executeDirectStack(stack, event, true) || handled; + break; + } + } + + return handled; + } + + private static final class DeferredEffectEvent { + private final WiredEvent event; + private final boolean negateConditions; + + private DeferredEffectEvent(WiredEvent event, boolean negateConditions) { + this.event = event; + this.negateConditions = negateConditions; + } + } + // ========== Reward System ========== /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java index 813b3f15..6b552b65 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java @@ -981,7 +981,7 @@ public final class WiredMoveCarryHelper { return new ArrayList<>(); } - return WiredSourceUtil.resolveItems(ctx, sourceType, null); + return WiredSourceUtil.resolveItemsRaw(ctx, sourceType, null); } private static Collection resolvePhysicsUsers(Room room, WiredContext ctx, int userSource) { @@ -997,7 +997,7 @@ public final class WiredMoveCarryHelper { return new ArrayList<>(); } - return WiredSourceUtil.resolveUsers(ctx, userSource); + return WiredSourceUtil.resolveUsersRaw(ctx, userSource); } private static WiredExtraMovePhysics getMovementPhysicsExtra(Room room, HabboItem stackItem) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java index 671ec052..00e41a15 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java @@ -29,31 +29,10 @@ public final class WiredSourceUtil { } public static List resolveItems(WiredContext ctx, int sourceType, Collection selectedItems) { - List resolvedItems; + List resolvedItems = resolveItemsInternal(ctx, sourceType, selectedItems, false); - switch (sourceType) { - case SOURCE_TRIGGER: - resolvedItems = ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); - break; - case SOURCE_SELECTED: - resolvedItems = (selectedItems != null) ? new ArrayList<>(selectedItems) : Collections.emptyList(); - break; - case SOURCE_SELECTOR: - WiredTargets itemTargets = getSelectorTargets(ctx); - resolvedItems = itemTargets.isItemsModifiedBySelector() - ? new ArrayList<>(itemTargets.items()) - : Collections.emptyList(); - break; - case SOURCE_SIGNAL: - if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { - resolvedItems = ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); - break; - } - resolvedItems = Collections.emptyList(); - break; - default: - resolvedItems = ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); - break; + if (ctx == null) { + return resolvedItems; } return (sourceType == SOURCE_SELECTOR) @@ -61,43 +40,19 @@ public final class WiredSourceUtil { : WiredSelectionFilterSupport.filterItems(ctx.room(), ctx.triggerItem(), ctx, resolvedItems); } + public static List resolveItemsRaw(WiredContext ctx, int sourceType, Collection selectedItems) { + return resolveItemsInternal(ctx, sourceType, selectedItems, true); + } + public static List resolveUsers(WiredContext ctx, int sourceType) { return resolveUsers(ctx, sourceType, null); } public static List resolveUsers(WiredContext ctx, int sourceType, Collection selectedUsers) { - List resolvedUsers; + List resolvedUsers = resolveUsersInternal(ctx, sourceType, selectedUsers); - switch (sourceType) { - case SOURCE_TRIGGER: - resolvedUsers = ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); - break; - case SOURCE_CLICKED_USER: - if (ctx.eventType() == WiredEvent.Type.USER_CLICKS_USER) { - resolvedUsers = ctx.event().getTargetUnit().map(Collections::singletonList).orElse(Collections.emptyList()); - break; - } - resolvedUsers = Collections.emptyList(); - break; - case SOURCE_SELECTED: - resolvedUsers = (selectedUsers != null) ? new ArrayList<>(selectedUsers) : Collections.emptyList(); - break; - case SOURCE_SELECTOR: - WiredTargets userTargets = getSelectorTargets(ctx); - resolvedUsers = userTargets.isUsersModifiedBySelector() - ? new ArrayList<>(userTargets.users()) - : Collections.emptyList(); - break; - case SOURCE_SIGNAL: - if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { - resolvedUsers = ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); - break; - } - resolvedUsers = Collections.emptyList(); - break; - default: - resolvedUsers = ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); - break; + if (ctx == null) { + return resolvedUsers; } return (sourceType == SOURCE_SELECTOR) @@ -105,6 +60,14 @@ public final class WiredSourceUtil { : WiredSelectionFilterSupport.filterUsers(ctx.room(), ctx.triggerItem(), ctx, resolvedUsers); } + public static List resolveUsersRaw(WiredContext ctx, int sourceType) { + return resolveUsersRaw(ctx, sourceType, null); + } + + public static List resolveUsersRaw(WiredContext ctx, int sourceType, Collection selectedUsers) { + return resolveUsersInternal(ctx, sourceType, selectedUsers); + } + public static boolean isDefaultUserSource(int value) { switch (value) { case SOURCE_TRIGGER: @@ -251,4 +214,75 @@ public final class WiredSourceUtil { private static void applySelectionFilterExtras(Room room, HabboItem triggerItem, WiredContext selectorCtx) { WiredSelectionFilterSupport.applySelectorFilters(room, triggerItem, selectorCtx); } + + private static List resolveItemsInternal(WiredContext ctx, int sourceType, Collection selectedItems, boolean allowTriggerItemFallback) { + if (ctx == null) { + return Collections.emptyList(); + } + + switch (sourceType) { + case SOURCE_TRIGGER: + return resolveTriggerItems(ctx, allowTriggerItemFallback); + case SOURCE_SELECTED: + return (selectedItems != null) ? new ArrayList<>(selectedItems) : Collections.emptyList(); + case SOURCE_SELECTOR: + WiredTargets itemTargets = getSelectorTargets(ctx); + return itemTargets.isItemsModifiedBySelector() + ? new ArrayList<>(itemTargets.items()) + : Collections.emptyList(); + case SOURCE_SIGNAL: + if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { + return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + } + return Collections.emptyList(); + default: + return resolveTriggerItems(ctx, allowTriggerItemFallback); + } + } + + private static List resolveUsersInternal(WiredContext ctx, int sourceType, Collection selectedUsers) { + if (ctx == null) { + return Collections.emptyList(); + } + + switch (sourceType) { + case SOURCE_TRIGGER: + return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + case SOURCE_CLICKED_USER: + if (ctx.eventType() == WiredEvent.Type.USER_CLICKS_USER) { + return ctx.event().getTargetUnit().map(Collections::singletonList).orElse(Collections.emptyList()); + } + return Collections.emptyList(); + case SOURCE_SELECTED: + return (selectedUsers != null) ? new ArrayList<>(selectedUsers) : Collections.emptyList(); + case SOURCE_SELECTOR: + WiredTargets userTargets = getSelectorTargets(ctx); + return userTargets.isUsersModifiedBySelector() + ? new ArrayList<>(userTargets.users()) + : Collections.emptyList(); + case SOURCE_SIGNAL: + if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { + return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + } + return Collections.emptyList(); + default: + return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + } + } + + private static List resolveTriggerItems(WiredContext ctx, boolean allowTriggerItemFallback) { + if (ctx == null) { + return Collections.emptyList(); + } + + if (ctx.sourceItem().isPresent()) { + return Collections.singletonList(ctx.sourceItem().get()); + } + + if (allowTriggerItemFallback && ctx.triggerItem() != null) { + return Collections.singletonList(ctx.triggerItem()); + } + + return Collections.emptyList(); + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java index 50aae6d9..38764d79 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java @@ -5,6 +5,11 @@ import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariable import com.eu.habbo.habbohotel.rooms.Room; import gnu.trove.set.hash.THashSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + public final class WiredVariableTextConnectorSupport { private WiredVariableTextConnectorSupport() { } @@ -18,31 +23,43 @@ public final class WiredVariableTextConnectorSupport { } public static WiredExtraVariableTextConnector getConnector(Room room, int definitionItemId) { + List connectors = getConnectors(room, definitionItemId); + return connectors.isEmpty() ? null : connectors.get(0); + } + + public static List getConnectors(Room room, int definitionItemId) { if (room == null || room.getRoomSpecialTypes() == null || definitionItemId <= 0) { - return null; + return Collections.emptyList(); } InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(definitionItemId); - return getConnector(room, extra); + return getConnectors(room, extra); } public static WiredExtraVariableTextConnector getConnector(Room room, InteractionWiredExtra definition) { + List connectors = getConnectors(room, definition); + return connectors.isEmpty() ? null : connectors.get(0); + } + + public static List getConnectors(Room room, InteractionWiredExtra definition) { if (room == null || definition == null || room.getRoomSpecialTypes() == null) { - return null; + return Collections.emptyList(); } THashSet extras = room.getRoomSpecialTypes().getExtras(definition.getX(), definition.getY()); if (extras == null || extras.isEmpty()) { - return null; + return Collections.emptyList(); } + List connectors = new ArrayList<>(); + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { if (extra instanceof WiredExtraVariableTextConnector) { - return (WiredExtraVariableTextConnector) extra; + connectors.add((WiredExtraVariableTextConnector) extra); } } - return null; + return connectors; } public static String toText(Room room, int definitionItemId, Integer value) { @@ -50,12 +67,34 @@ public final class WiredVariableTextConnectorSupport { return ""; } - WiredExtraVariableTextConnector connector = getConnector(room, definitionItemId); - return connector != null ? connector.resolveText(value) : String.valueOf(value); + for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) { + Map mappings = connector.getMappings(); + if (mappings.containsKey(value)) { + String mappedValue = mappings.get(value); + return mappedValue != null ? mappedValue : String.valueOf(value); + } + } + + return String.valueOf(value); } public static Integer toValue(Room room, int definitionItemId, String text) { - WiredExtraVariableTextConnector connector = getConnector(room, definitionItemId); - return connector != null ? connector.resolveValue(text) : null; + if (text == null) { + return null; + } + + String normalizedText = text.trim(); + if (normalizedText.isEmpty()) { + return null; + } + + for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) { + Integer mappedValue = connector.resolveValue(normalizedText); + if (mappedValue != null) { + return mappedValue; + } + } + + return null; } } From ae08d4b3f4d93c114cbc20bd333b7e31df599cd5 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Mon, 13 Apr 2026 16:45:40 +0200 Subject: [PATCH 8/8] Preserve signal origin actor context --- .../wired/effects/WiredEffectSendSignal.java | 37 ++- .../habbohotel/wired/core/WiredEvent.java | 19 ++ docs/wired_send_signal_flow.html | 283 ++++++++++++++++++ 3 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 docs/wired_send_signal_flow.html diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index 460b6936..f9121ce5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -103,30 +103,52 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } LOGGER.debug("[SendSignal] Resolved {} antenna(s), firing signals", resolvedAntennas.size()); + RoomUnit triggeringUser = ctx.event().getOriginActor().orElseGet(() -> ctx.actor().orElse(null)); List forwardedUsers = WiredSourceUtil.resolveUsersRaw(ctx, this.userForward); List forwardedFurni = WiredSourceUtil.resolveItemsRaw(ctx, this.furniForward, this.forwardItems); - RoomUnit defaultUser = forwardedUsers.isEmpty() ? null : forwardedUsers.get(0); - Collection usersToSend = (signalPerUser && !forwardedUsers.isEmpty()) - ? forwardedUsers - : Collections.singletonList(defaultUser); + List usersToSend; + if (signalPerUser) { + LinkedHashMap mergedUsers = new LinkedHashMap<>(); + + if (triggeringUser != null) { + mergedUsers.put(triggeringUser.getId(), triggeringUser); + } + + for (RoomUnit forwardedUser : forwardedUsers) { + if (forwardedUser == null) { + continue; + } + + mergedUsers.put(forwardedUser.getId(), forwardedUser); + } + + usersToSend = mergedUsers.isEmpty() + ? Collections.singletonList(null) + : new ArrayList<>(mergedUsers.values()); + } else { + usersToSend = Collections.singletonList(triggeringUser); + } Collection furniToSend = !forwardedFurni.isEmpty() ? forwardedFurni : Collections.singletonList(null); int nextDepth = currentDepth + 1; + int signalUserCount = signalPerUser + ? (int) usersToSend.stream().filter(Objects::nonNull).count() + : (!forwardedUsers.isEmpty() ? forwardedUsers.size() : (triggeringUser != null ? 1 : 0)); for (RoomUnit user : usersToSend) { for (HabboItem sourceItem : furniToSend) { for (HabboItem antenna : resolvedAntennas) { - fireSignalAtAntenna(ctx, room, antenna, user, sourceItem, nextDepth); + fireSignalAtAntenna(ctx, room, antenna, user, triggeringUser, sourceItem, signalUserCount, nextDepth); } } } } - private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth) { + private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, RoomUnit originActor, HabboItem sourceItem, int signalUserCount, int depth) { if (antenna == null) return; RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); if (tile == null) return; @@ -142,12 +164,13 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .tile(tile) .callStackDepth(depth) .signalChannel(signalChannel) - .signalUserCount(actor != null ? 1 : 0) + .signalUserCount(signalUserCount) .signalFurniCount(sourceItem != null ? 1 : 0) .contextVariableScope(ctx.contextVariables()) .triggeredByEffect(true); if (actor != null) builder.actor(actor); + if (originActor != null) builder.originActor(originActor); if (sourceItem != null) builder.sourceItem(sourceItem); boolean result = dispatchSignalEvent(builder.build()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java index 0a18ee86..59ad10fd 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java @@ -163,6 +163,7 @@ public final class WiredEvent { private final Type type; private final Room room; private final RoomUnit actor; // nullable - the user/bot that caused the event + private final RoomUnit originActor; // nullable - original user that started the chain, preserved across signals private final HabboItem sourceItem; // nullable - the furniture involved private final RoomTile tile; // nullable - the tile where event occurred private final String text; // nullable - text for say triggers @@ -191,6 +192,7 @@ public final class WiredEvent { this.room = builder.room; this.actor = builder.actor; this.sourceItem = builder.sourceItem; + this.originActor = builder.originActor; this.tile = builder.tile; this.text = builder.text; this.targetUnit = builder.targetUnit; @@ -240,6 +242,17 @@ public final class WiredEvent { return Optional.ofNullable(actor); } + /** + * Get the original actor that started the current event chain. + * For signal events this can differ from {@link #getActor()} when the + * signal forwards users one-by-one but still needs to preserve who + * originally triggered the chain. + * @return optional containing the original actor, or empty if unavailable + */ + public Optional getOriginActor() { + return Optional.ofNullable(originActor); + } + /** * Get the source item that was involved in this event. * For example, the furniture that was clicked or stepped on. @@ -407,6 +420,7 @@ public final class WiredEvent { private final Type type; private final Room room; private RoomUnit actor; + private RoomUnit originActor; private HabboItem sourceItem; private RoomTile tile; private String text; @@ -447,6 +461,11 @@ public final class WiredEvent { return this; } + public Builder originActor(RoomUnit originActor) { + this.originActor = originActor; + return this; + } + /** * Set the source item involved in this event. * @param sourceItem the habbo item diff --git a/docs/wired_send_signal_flow.html b/docs/wired_send_signal_flow.html new file mode 100644 index 00000000..79dd9f69 --- /dev/null +++ b/docs/wired_send_signal_flow.html @@ -0,0 +1,283 @@ + + + + + + Wired Send Signal - Flow Attuale + + + +
+
+ + Wired · Send Signal + +

Schema del flow attuale

+

+ Questa pagina descrive il comportamento attuale del wired send signal, con tutti i casi principali: + antenne, utenti, furni, conteggi e combinazioni finali. È pensata da inoltrare così com'è per un controllo del flow. +

+
+ +
+
+

Formula finale

+

antenne × utenti × furni

+

+ Il numero totale di segnali emessi è dato dal prodotto tra antenne valide, rami utente e rami furni. +

+
+
+

Regola utenti

+

+ Con per ogni utente = disattivo, il ramo usa sempre l'utente che innesca. + Con per ogni utente = attivo, il ramo usa l'utente che innesca più gli utenti trovati dalla source. +

+
+
+

Regola furni

+

+ Se la source furni restituisce elementi, il flow attuale apre un ramo per ogni furni. + Se non restituisce nulla, viene emesso un solo ramo senza furni allegati. +

+
+
+ +
+

Pseudo flow

+
    +
  1. 1. Vengono risolte le antenne destinazione.
  2. +
  3. 2. Restano valide solo le antenne reali; se non ne resta nessuna, il flow si ferma.
  4. +
  5. 3. Viene preso l'utente che ha innescato, se esiste.
  6. +
  7. 4. Vengono risolti gli utenti dalla source utenti.
  8. +
  9. 5. Vengono risolti i furni dalla source furni.
  10. +
  11. 6. Se “per ogni utente” è attivo, si costruisce una lista unica con: +
      +
    • sempre l'utente che innesca, se presente;
    • +
    • poi tutti gli utenti della source;
    • +
    • senza duplicati.
    • +
    +
  12. +
  13. 7. Se “per ogni utente” è disattivo, si usa un solo ramo utente: + l'utente che innesca.
  14. +
  15. 8. Se la source furni ha elementi, si apre un ramo per ogni furni.
  16. +
  17. 9. Se la source furni è vuota, si apre un solo ramo senza furni.
  18. +
  19. 10. Per ogni combinazione antenna + utente + furni viene emesso un segnale separato.
  20. +
  21. 11. Ogni segnale porta con sé: +
      +
    • la tile dell'antenna;
    • +
    • l'utente del ramo corrente, se presente;
    • +
    • l'utente originario che ha innescato la chain, se presente;
    • +
    • il furni del ramo corrente, se presente;
    • +
    • le context variables;
    • +
    • la profondità della chain aggiornata;
    • +
    • i conteggi utenti/furni esposti al ramo ricevente.
    • +
    +
  22. +
+
+ +
+
+

Tabella casi · Utenti

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Per ogni utenteUtente che innescaSource utentiRami utente emessiNota
DisattivoPresenteVuota1Parte sempre con l'utente che innesca.
DisattivoPresente3 utenti1Gli utenti source non diventano rami separati.
DisattivoAssenteVuota1Parte un ramo senza utente.
AttivoPresenteVuota1L'utente che innesca viene sempre incluso.
AttivoPresente3 utenti diversi4Utente che innesca + 3 utenti della source.
AttivoPresenteContiene già l'utente che innescaUtenti uniciNessun duplicato.
AttivoAssente3 utenti3Usa solo gli utenti trovati dalla source.
AttivoAssenteVuota1Parte un ramo senza utente.
+
+
+ +
+

Tabella casi · Furni

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Source furniFurni trovatiRami furni emessiDato nel singolo ramo
Vuota01Nessun furni allegato
1 furni11Quel furni
3 furni33Un furni diverso per ramo
7 furni77Un furni diverso per ramo
+
+

+ Nel comportamento attuale, se la source furni restituisce elementi, il flow si apre sempre per furni singolo. +

+
+
+ +
+

Tabella casi · Combinazioni complete

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CasoAntenneRami utenteRami furniTotale segnali
Utente che innesca presente, per ogni utente disattivo, 3 furni2136
Utente che innesca presente, per ogni utente attivo, source utenti con 3 utenti, 3 furni24324
Utente che innesca presente, selector utenti vuoto, 7 furni1177
Nessun utente, source utenti vuota, 7 furni1177
Nessuna antenna valida0qualsiasiqualsiasi0
+
+
+ +
+
+

Conteggi esposti al ricevente

+
    +
  • Conteggio utenti con “per ogni utente” attivo: numero di utenti unici del merge tra utente che innesca e source utenti.
  • +
  • Conteggio utenti con “per ogni utente” disattivo: se la source utenti ha elementi, vale il numero di utenti trovati dalla source; altrimenti vale 1 se esiste l'utente che innesca, altrimenti 0.
  • +
  • Conteggio furni: nel singolo ramo vale 1 se c'è un furni allegato, altrimenti 0.
  • +
+
+
+

Nota importante sul comportamento attuale

+

+ Oggi il flow reale fa fan-out per furni quando la source furni restituisce elementi. Quindi, se dalla source arrivano 7 furni, + il sistema apre 7 rami furni distinti. Questo è importante perché impatta sia il numero totale dei segnali sia i conteggi + osservati a valle. +

+

+ Inoltre il segnale conserva anche l'utente originario che ha avviato la chain, separato dall'utente del ramo corrente. +

+
+
+ + +
+ +