You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 07:26:18 +00:00
Compare commits
325 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38e7b6e880 | |||
| d451a3dc71 | |||
| caaff4f591 | |||
| a89d18e8de | |||
| 733cce25d7 | |||
| 9bc5ad5fb0 | |||
| ee9feb95fd | |||
| 0e650733df | |||
| 518957ddde | |||
| 39168c27c8 | |||
| 8d31a4a81b | |||
| 4dac2b72bc | |||
| a2fb6c820e | |||
| d023d69524 | |||
| 365d3258f3 | |||
| a91aac7538 | |||
| fe57dee67f | |||
| 1775f0b222 | |||
| 60a15c96a0 | |||
| f2aaa2b472 | |||
| b839aec641 | |||
| 982fe35863 | |||
| 6b41b9dc47 | |||
| 673467e81c | |||
| 36772b6e6f | |||
| 70adcd7b18 | |||
| ee85888db3 | |||
| fa942d3623 | |||
| 8102fbf053 | |||
| 5ccef29944 | |||
| a05f61703f | |||
| 07bfc46e66 | |||
| f290a99704 | |||
| ea7daf713c | |||
| 2ac1cad96b | |||
| 1116602c44 | |||
| 867b146afd | |||
| a6a4015211 | |||
| 4db976e46e | |||
| 3ee5c1079a | |||
| d044ce1604 | |||
| 463ae925dd | |||
| f25c2a8485 | |||
| b88635a0a7 | |||
| 9004538ec4 | |||
| 804c2e639a | |||
| 4855ea6ea5 | |||
| c73eb49145 | |||
| ece9a214db | |||
| 6b4da4f916 | |||
| 61b5cdee37 | |||
| 4f2a5999e4 | |||
| 7cf83cf158 | |||
| 4ceac0bbc6 | |||
| 5768cc3f01 | |||
| 18d90a635e | |||
| a90a8754bc | |||
| 305d1ca097 | |||
| f292426529 | |||
| a2d75ea487 | |||
| 60b998f909 | |||
| fbe9eddb37 | |||
| 80c8467f82 | |||
| 23e6d4800a | |||
| c83cd22fff | |||
| 29ed72fca9 | |||
| 25e38fbbeb | |||
| 60eeaca689 | |||
| dc6a912632 | |||
| a2c958684c | |||
| 237c3a3425 | |||
| 3304edafb7 | |||
| cf307e44da | |||
| 5dfa8df5f4 | |||
| a5dabd924e | |||
| 4479763f12 | |||
| 7869938d98 | |||
| 916b9df7a3 | |||
| 0ceeb5ca70 | |||
| 2f46f31684 | |||
| 240fede12a | |||
| 0109c25c80 | |||
| 5293b7fd5d | |||
| b5891ae7da | |||
| b580c67dcf | |||
| 18a37fa92e | |||
| b846f066e4 | |||
| 65cb89dc84 | |||
| f2a010b25c | |||
| 42708c0b07 | |||
| 12753dad2b | |||
| c094f440b2 | |||
| b9d5fa2557 | |||
| 4d4c796ad5 | |||
| cc8efb7382 | |||
| 272aab8309 | |||
| f9fbbb0355 | |||
| 2dac462f81 | |||
| 159ce25a88 | |||
| 2ef2c59c80 | |||
| 0a9753a5c0 | |||
| 40ae702e51 | |||
| 1af212191e | |||
| 7d0aacc490 | |||
| c75058d4e8 | |||
| 1d7c5b856f | |||
| 743cad8361 | |||
| a37de4556b | |||
| 62454671d2 | |||
| efe7897fb4 | |||
| 032003b64c | |||
| e24020e9df | |||
| ba80870df0 | |||
| 3342b22a76 | |||
| 5b2c9f0aee | |||
| 26f86e3e31 | |||
| 9b9902c76d | |||
| 112796e133 | |||
| 736b7c70b4 | |||
| b0d4317c2d | |||
| 4cf0af79d1 | |||
| 547c5ef157 | |||
| a433e5539d | |||
| b600ac499c | |||
| 1598297a2a | |||
| 37ce71ad1e | |||
| 416d0bb088 | |||
| 9c3d887447 | |||
| 316613db6e | |||
| 5f4e91133e | |||
| 47dcbae4b3 | |||
| cdc0620c9b | |||
| 827b130ccc | |||
| b7f153f8e7 | |||
| bea385afe2 | |||
| 8c7d6db135 | |||
| 95bd84a95f | |||
| 22b05b4e52 | |||
| 766d8d67d3 | |||
| bd9657cf63 | |||
| e29e06201c | |||
| dac83e8a62 | |||
| 916ef7af3a | |||
| 044d1141cd | |||
| c98261d8c3 | |||
| 8ba9132e7e | |||
| 36a06647f0 | |||
| c48e01cb8e | |||
| 916cf9ba9e | |||
| 0af489cef2 | |||
| 6171ec7bab | |||
| c048713b22 | |||
| e5e3918513 | |||
| 14593b4638 | |||
| c199d805d8 | |||
| 3282430b67 | |||
| 560def21d7 | |||
| 5011fdf848 | |||
| d34b44a656 | |||
| 848b8bd5ce | |||
| 80400f828c | |||
| 6868dd8d3d | |||
| 9f1e036310 | |||
| ec24283e0f | |||
| 93c4565660 | |||
| 31027095ec | |||
| aa6dcd1062 | |||
| 11554eae7b | |||
| 25273679a1 | |||
| 15b56f9519 | |||
| 8412a51ec4 | |||
| 5d8dc670bd | |||
| 81c8dfc605 | |||
| 4747699656 | |||
| dba0337a7b | |||
| 3cb24a5185 | |||
| 775197984f | |||
| 4eafb54c57 | |||
| d8260ec461 | |||
| b94acdf719 | |||
| 4330bf5a62 | |||
| aaad94f954 | |||
| d9cf70910f | |||
| fe0ba3b9e9 | |||
| 4b81997e62 | |||
| 79d734ef26 | |||
| dbcf139a52 | |||
| 98aab95d58 | |||
| fb85952e88 | |||
| 54ef2ee251 | |||
| df2a849adc | |||
| 8e21765676 | |||
| 0081280328 | |||
| 2bc4340ec9 | |||
| 93e5ea15aa | |||
| aec61064ae | |||
| 8db6281cc8 | |||
| 8672c2d0ea | |||
| a92feb2ef0 | |||
| 478c4c70b8 | |||
| 7ba0029ba8 | |||
| 39c6e24097 | |||
| 2b18ca2deb | |||
| 9ac50600f6 | |||
| edddc551c5 | |||
| 1a03b8f3a9 | |||
| d7fa02a453 | |||
| 994d539caf | |||
| c6e43c6d55 | |||
| 61972dafa4 | |||
| 14a590235c | |||
| 39d21daeff | |||
| c9214bac07 | |||
| fdcd3a7323 | |||
| 7a7e38311d | |||
| 4359650621 | |||
| 82c6f3f9ff | |||
| 60ccc8c80b | |||
| eb41e3afb9 | |||
| a8e0534634 | |||
| 98e366dd07 | |||
| 9edb984f56 | |||
| ea55258979 | |||
| 16d89cdb31 | |||
| ede7eb8284 | |||
| 216078f62c | |||
| 0f15371676 | |||
| c25cb2a9b6 | |||
| 87e1ef94f7 | |||
| 510e0d082e | |||
| e13c7fdbb6 | |||
| 2a28fbd2e5 | |||
| cd60cba355 | |||
| e62f461962 | |||
| 7f8c98e4f3 | |||
| d95e09e64f | |||
| ebe0690e46 | |||
| 0dda0ae0f7 | |||
| 54ab6613f1 | |||
| 9fda766ba5 | |||
| 3da9325344 | |||
| 770739c256 | |||
| 3ec468993a | |||
| 0e0f1cbb15 | |||
| daeda761cd | |||
| 0906048a3a | |||
| 19cde45d3e | |||
| 8161e3d7e5 | |||
| 5c0f2d2855 | |||
| d984461cc0 | |||
| 61ea33ac28 | |||
| b6ee400b83 | |||
| 62104596ac | |||
| fad6be158a | |||
| a9f1903465 | |||
| af82352f24 | |||
| dcc23ba744 | |||
| f7556138aa | |||
| a0910d822c | |||
| 4eb1484daf | |||
| 45d01876c1 | |||
| 1c4449fb88 | |||
| 373d0399c1 | |||
| 01c17c0511 | |||
| d1570d3574 | |||
| c98d3a3205 | |||
| da1fd01074 | |||
| f7bd452cb0 | |||
| 48fcd3f78b | |||
| 1275254fa0 | |||
| 7ed7a1ec5a | |||
| d383c43bbf | |||
| 44bfcc49b4 | |||
| b0ffb64cb2 | |||
| 1f4eef8e2e | |||
| bfc6ff21a5 | |||
| ea88934e9e | |||
| bb4b9fb7f4 | |||
| 84d7968b76 | |||
| f5bf4baa79 | |||
| 4a02d22061 | |||
| 14854efaeb | |||
| 564c8d647e | |||
| 0e7138a721 | |||
| 76eb1ecd05 | |||
| 4621ed62b7 | |||
| 2b8ce3cd91 | |||
| 57c36da795 | |||
| 17629c210c | |||
| 50444003bb | |||
| f55b182d8e | |||
| 1416cd7464 | |||
| 392d24b9c5 | |||
| 9dcd58d027 | |||
| 3b85d5fa34 | |||
| 43c2c2b0f1 | |||
| a815c1b99d | |||
| caf6ad35fa | |||
| 258a95a269 | |||
| 4944d41410 | |||
| 8fb117ae73 | |||
| 7f4f7d6da9 | |||
| 0cf46471f2 | |||
| 3a505cd559 | |||
| f2e0f6e2d5 | |||
| d73573e7c5 | |||
| efb88e5957 | |||
| e7e75a285b | |||
| 28c3e93945 | |||
| 5bf1d42cfb | |||
| b162b3f4d8 | |||
| 86498b6b4c | |||
| 964f388594 | |||
| f9644d83b7 | |||
| 0b142d184c | |||
| 867c8ff857 | |||
| 5094d6ce4f | |||
| 2c0ef9873c | |||
| dadc1b8aaf | |||
| 85758b53fa | |||
| 2171b5f2ec | |||
| 46306c8205 | |||
| fadec887cd | |||
| e614c1d64f | |||
| e7deea7d9d |
@@ -3,6 +3,7 @@ name: Build & Release JAR
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -435,6 +435,16 @@ ON DUPLICATE KEY UPDATE
|
|||||||
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
|
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
|
||||||
|
|
||||||
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||||
|
('commands.description.acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'),
|
||||||
|
('commands.description.cmd_add_youtube_playlist', ':add_youtube <base_item_id> <youtube_playlist_id>'),
|
||||||
|
('commands.description.cmd_disablemassmentions', ':disablemassmentions'),
|
||||||
|
('commands.description.cmd_disablementions', ':disablementions'),
|
||||||
|
('commands.description.cmd_give_prefix', ':giveprefix <username> <text> <color> [icon] [effect]'),
|
||||||
|
('commands.description.cmd_hidewired', ':hidewired'),
|
||||||
|
('commands.description.cmd_list_prefixes', ':listprefixes <username>'),
|
||||||
|
('commands.description.cmd_remove_prefix', ':removeprefix <username> <id|all>'),
|
||||||
|
('commands.description.cmd_setroom_template', ':setroom_template'),
|
||||||
|
('commands.description.cmd_update_youtube_playlists', ':update_youtube'),
|
||||||
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
|
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
|
||||||
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
|
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
|
||||||
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ VALUES
|
|||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`comment` = VALUES(`comment`);
|
`comment` = VALUES(`comment`);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||||
|
('commands.description.cmd_disablementions', ':disablementions'),
|
||||||
|
('commands.description.cmd_disablemassmentions', ':disablemassmentions');
|
||||||
|
|
||||||
|
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
-- 3. Emulator settings: cooldowns, caps and alias lists
|
-- 3. Emulator settings: cooldowns, caps and alias lists
|
||||||
@@ -74,3 +78,16 @@ INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
|||||||
ALTER TABLE `wordfilter`
|
ALTER TABLE `wordfilter`
|
||||||
ADD COLUMN `prefix_only` ENUM('0','1') NOT NULL DEFAULT '0'
|
ADD COLUMN `prefix_only` ENUM('0','1') NOT NULL DEFAULT '0'
|
||||||
COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`;
|
COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- 5. Per-user mention preferences (:disablementions / :disablemassmentions)
|
||||||
|
--
|
||||||
|
-- Read by HabboStats (default '1' = enabled), toggled by the commands.
|
||||||
|
-- Without these columns the toggle commands cannot persist.
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ALTER TABLE `users_settings`
|
||||||
|
ADD COLUMN IF NOT EXISTS `mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
|
||||||
|
COMMENT 'Receive @nick mention notifications.',
|
||||||
|
ADD COLUMN IF NOT EXISTS `mass_mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
|
||||||
|
COMMENT 'Receive broadcast (@all / @friends / @room) mentions.';
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- 020_furnidata_edit_log.sql
|
||||||
|
-- Audit trail for furnidata name/description edits made through the furni editor,
|
||||||
|
-- plus config keys for the editor write path. NOTE: *.enabled keys elsewhere are
|
||||||
|
-- read via Boolean.parseBoolean (true/false), but these two are numeric.
|
||||||
|
CREATE TABLE IF NOT EXISTS `furnidata_edit_log` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(11) NOT NULL,
|
||||||
|
`classname` varchar(255) NOT NULL,
|
||||||
|
`action` enum('edit','revert') NOT NULL DEFAULT 'edit',
|
||||||
|
`old_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`old_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`timestamp` int(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_classname` (`classname`),
|
||||||
|
INDEX `idx_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
|
||||||
|
('items.furnidata.edit.backup.keep','10'),
|
||||||
|
('items.furnidata.edit.ratelimit.ms','2000'),
|
||||||
|
-- Server-authoritative furni names (source of truth = furnidata JSON)
|
||||||
|
('items.furnidata.names.enabled','true'),
|
||||||
|
('items.furnidata.path',''),
|
||||||
|
('items.furnidata.max.bytes','67108864'),
|
||||||
|
-- Live-reload watcher
|
||||||
|
('items.furnidata.watch.enabled','true'),
|
||||||
|
('items.furnidata.watch.debounce.ms','750'),
|
||||||
|
('items.furnidata.watch.min.interval.ms','5000'),
|
||||||
|
('items.furnidata.delta.cap','500'),
|
||||||
|
-- Furni editor: import official names/descriptions from Habbo
|
||||||
|
('furni.editor.import.url','https://www.habbo.com/gamedata/furnidata_json/1'),
|
||||||
|
('furni.editor.import.cache.ms','600000');
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS cleanup_furnidata_settings;
|
||||||
|
CREATE TEMPORARY TABLE cleanup_furnidata_settings (
|
||||||
|
`key` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL PRIMARY KEY
|
||||||
|
);
|
||||||
|
INSERT INTO cleanup_furnidata_settings (`key`) VALUES
|
||||||
|
('items.furnidata.names.enabled'),
|
||||||
|
('items.furnidata.path'),
|
||||||
|
('items.furnidata.max.bytes'),
|
||||||
|
('items.furnidata.watch.enabled'),
|
||||||
|
('items.furnidata.watch.debounce.ms'),
|
||||||
|
('items.furnidata.watch.min.interval.ms'),
|
||||||
|
('items.furnidata.delta.cap'),
|
||||||
|
('furni.editor.import.url'),
|
||||||
|
('furni.editor.import.cache.ms');
|
||||||
|
|
||||||
|
-- Preview rows that will be removed.
|
||||||
|
SELECT es.`key`, es.`value`
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`
|
||||||
|
ORDER BY es.`key`;
|
||||||
|
|
||||||
|
DELETE es
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Preview remaining matching rows inside the transaction.
|
||||||
|
SELECT COUNT(*) AS remaining_furnidata_settings
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Safe default. Change to COMMIT after reviewing the preview.
|
||||||
|
ROLLBACK;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Navigator search filters - companion to the gameserver fix for the catalog
|
||||||
|
-- 'Find groups to join!' button (navigator/search/hotel_view/group:).
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `navigator_filter` (`key`, `field`, `compare`, `database_query`) VALUES
|
||||||
|
('anything', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
|
||||||
|
('roomname', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
|
||||||
|
('owner', 'getOwnerName', 'equals_ignore_case', 'SELECT rooms.* FROM rooms WHERE rooms.owner_name LIKE ?'),
|
||||||
|
('tag', 'getTags', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.tags LIKE ?'),
|
||||||
|
('group', 'getGuildName', 'contains', 'SELECT rooms.* FROM rooms INNER JOIN guilds ON guilds.room_id = rooms.id WHERE guilds.name LIKE ?');
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `users_earnings_claims` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(11) NOT NULL,
|
||||||
|
`category` varchar(64) NOT NULL,
|
||||||
|
`period_key` varchar(32) NOT NULL,
|
||||||
|
`claimed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `users_earnings_claims_unique_period` (`user_id`, `category`, `period_key`),
|
||||||
|
KEY `users_earnings_claims_user_id` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||||
|
('earnings.enabled', '0', 'Enable the emulator-owned earnings center reward hub.'),
|
||||||
|
('earnings.daily_gift.enabled', '1', 'Enable daily gift earnings row.'),
|
||||||
|
('earnings.daily_gift.cooldown.seconds', '86400', 'Cooldown in seconds for daily gift earnings claims.'),
|
||||||
|
('earnings.daily_gift.credits', '0', 'Credits granted by daily gift earnings claims.'),
|
||||||
|
('earnings.daily_gift.pixels', '0', 'Pixels granted by daily gift earnings claims.'),
|
||||||
|
('earnings.daily_gift.points', '0', 'Seasonal points granted by daily gift earnings claims.'),
|
||||||
|
('earnings.daily_gift.points.type', '5', 'Seasonal point type granted by daily gift earnings claims.'),
|
||||||
|
('earnings.games.enabled', '1', 'Enable games earnings row.'),
|
||||||
|
('earnings.games.cooldown.seconds', '86400', 'Cooldown in seconds for games earnings claims.'),
|
||||||
|
('earnings.games.credits', '0', 'Credits granted by games earnings claims.'),
|
||||||
|
('earnings.games.pixels', '0', 'Pixels granted by games earnings claims.'),
|
||||||
|
('earnings.games.points', '0', 'Seasonal points granted by games earnings claims.'),
|
||||||
|
('earnings.games.points.type', '5', 'Seasonal point type granted by games earnings claims.'),
|
||||||
|
('earnings.achievements.enabled', '1', 'Enable achievements earnings row.'),
|
||||||
|
('earnings.achievements.cooldown.seconds', '86400', 'Cooldown in seconds for achievements earnings claims.'),
|
||||||
|
('earnings.achievements.credits', '0', 'Credits granted by achievements earnings claims.'),
|
||||||
|
('earnings.achievements.pixels', '0', 'Pixels granted by achievements earnings claims.'),
|
||||||
|
('earnings.achievements.points', '0', 'Seasonal points granted by achievements earnings claims.'),
|
||||||
|
('earnings.achievements.points.type', '5', 'Seasonal point type granted by achievements earnings claims.'),
|
||||||
|
('earnings.marketplace.enabled', '1', 'Enable marketplace earnings row.'),
|
||||||
|
('earnings.marketplace.cooldown.seconds', '86400', 'Cooldown in seconds for marketplace earnings claims.'),
|
||||||
|
('earnings.marketplace.credits', '0', 'Credits granted by marketplace earnings claims.'),
|
||||||
|
('earnings.marketplace.pixels', '0', 'Pixels granted by marketplace earnings claims.'),
|
||||||
|
('earnings.marketplace.points', '0', 'Seasonal points granted by marketplace earnings claims.'),
|
||||||
|
('earnings.marketplace.points.type', '5', 'Seasonal point type granted by marketplace earnings claims.'),
|
||||||
|
('earnings.hc_payday.enabled', '1', 'Enable HC payday earnings row.'),
|
||||||
|
('earnings.hc_payday.cooldown.seconds', '86400', 'Cooldown in seconds for HC payday earnings claims.'),
|
||||||
|
('earnings.hc_payday.credits', '0', 'Credits granted by HC payday earnings claims.'),
|
||||||
|
('earnings.hc_payday.pixels', '0', 'Pixels granted by HC payday earnings claims.'),
|
||||||
|
('earnings.hc_payday.points', '0', 'Seasonal points granted by HC payday earnings claims.'),
|
||||||
|
('earnings.hc_payday.points.type', '5', 'Seasonal point type granted by HC payday earnings claims.'),
|
||||||
|
('earnings.level_progress.enabled', '1', 'Enable level progress earnings row.'),
|
||||||
|
('earnings.level_progress.cooldown.seconds', '86400', 'Cooldown in seconds for level progress earnings claims.'),
|
||||||
|
('earnings.level_progress.credits', '0', 'Credits granted by level progress earnings claims.'),
|
||||||
|
('earnings.level_progress.pixels', '0', 'Pixels granted by level progress earnings claims.'),
|
||||||
|
('earnings.level_progress.points', '0', 'Seasonal points granted by level progress earnings claims.'),
|
||||||
|
('earnings.level_progress.points.type', '5', 'Seasonal point type granted by level progress earnings claims.'),
|
||||||
|
('earnings.donations.enabled', '1', 'Enable donations earnings row.'),
|
||||||
|
('earnings.donations.cooldown.seconds', '86400', 'Cooldown in seconds for donations earnings claims.'),
|
||||||
|
('earnings.donations.credits', '0', 'Credits granted by donations earnings claims.'),
|
||||||
|
('earnings.donations.pixels', '0', 'Pixels granted by donations earnings claims.'),
|
||||||
|
('earnings.donations.points', '0', 'Seasonal points granted by donations earnings claims.'),
|
||||||
|
('earnings.donations.points.type', '5', 'Seasonal point type granted by donations earnings claims.'),
|
||||||
|
('earnings.bonus_bag.enabled', '1', 'Enable bonus bag earnings row.'),
|
||||||
|
('earnings.bonus_bag.cooldown.seconds', '86400', 'Cooldown in seconds for bonus bag earnings claims.'),
|
||||||
|
('earnings.bonus_bag.credits', '0', 'Credits granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.bonus_bag.pixels', '0', 'Pixels granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.bonus_bag.points', '0', 'Seasonal points granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.bonus_bag.points.type', '5', 'Seasonal point type granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.enabled', '1', 'Enable mystery boxes earnings row.'),
|
||||||
|
('earnings.mystery_boxes.cooldown.seconds', '86400', 'Cooldown in seconds for mystery boxes earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.credits', '0', 'Credits granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.pixels', '0', 'Pixels granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.points', '0', 'Seasonal points granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.points.type', '5', 'Seasonal point type granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.club_job.enabled', '1', 'Enable club and job earnings row.'),
|
||||||
|
('earnings.club_job.cooldown.seconds', '86400', 'Cooldown in seconds for club and job earnings claims.'),
|
||||||
|
('earnings.club_job.credits', '0', 'Credits granted by club and job earnings claims.'),
|
||||||
|
('earnings.club_job.pixels', '0', 'Pixels granted by club and job earnings claims.'),
|
||||||
|
('earnings.club_job.points', '0', 'Seasonal points granted by club and job earnings claims.'),
|
||||||
|
('earnings.club_job.points.type', '5', 'Seasonal point type granted by club and job earnings claims.');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||||
|
('earnings.daily_gift.badge', '', 'Badge code granted by daily gift earnings claims.'),
|
||||||
|
('earnings.daily_gift.item_id', '0', 'Items base id granted by daily gift earnings claims.'),
|
||||||
|
('earnings.daily_gift.item.quantity', '1', 'Furni quantity granted by daily gift earnings claims.'),
|
||||||
|
('earnings.daily_gift.hc.days', '0', 'HC days granted by daily gift earnings claims.'),
|
||||||
|
('earnings.games.badge', '', 'Badge code granted by games earnings claims.'),
|
||||||
|
('earnings.games.item_id', '0', 'Items base id granted by games earnings claims.'),
|
||||||
|
('earnings.games.item.quantity', '1', 'Furni quantity granted by games earnings claims.'),
|
||||||
|
('earnings.games.hc.days', '0', 'HC days granted by games earnings claims.'),
|
||||||
|
('earnings.achievements.badge', '', 'Badge code granted by achievements earnings claims.'),
|
||||||
|
('earnings.achievements.item_id', '0', 'Items base id granted by achievements earnings claims.'),
|
||||||
|
('earnings.achievements.item.quantity', '1', 'Furni quantity granted by achievements earnings claims.'),
|
||||||
|
('earnings.achievements.hc.days', '0', 'HC days granted by achievements earnings claims.'),
|
||||||
|
('earnings.marketplace.badge', '', 'Badge code granted by marketplace earnings claims.'),
|
||||||
|
('earnings.marketplace.item_id', '0', 'Items base id granted by marketplace earnings claims.'),
|
||||||
|
('earnings.marketplace.item.quantity', '1', 'Furni quantity granted by marketplace earnings claims.'),
|
||||||
|
('earnings.marketplace.hc.days', '0', 'HC days granted by marketplace earnings claims.'),
|
||||||
|
('earnings.hc_payday.badge', '', 'Badge code granted by HC payday earnings claims.'),
|
||||||
|
('earnings.hc_payday.item_id', '0', 'Items base id granted by HC payday earnings claims.'),
|
||||||
|
('earnings.hc_payday.item.quantity', '1', 'Furni quantity granted by HC payday earnings claims.'),
|
||||||
|
('earnings.hc_payday.hc.days', '0', 'HC days granted by HC payday earnings claims.'),
|
||||||
|
('earnings.level_progress.badge', '', 'Badge code granted by level progress earnings claims.'),
|
||||||
|
('earnings.level_progress.item_id', '0', 'Items base id granted by level progress earnings claims.'),
|
||||||
|
('earnings.level_progress.item.quantity', '1', 'Furni quantity granted by level progress earnings claims.'),
|
||||||
|
('earnings.level_progress.hc.days', '0', 'HC days granted by level progress earnings claims.'),
|
||||||
|
('earnings.donations.badge', '', 'Badge code granted by donations earnings claims.'),
|
||||||
|
('earnings.donations.item_id', '0', 'Items base id granted by donations earnings claims.'),
|
||||||
|
('earnings.donations.item.quantity', '1', 'Furni quantity granted by donations earnings claims.'),
|
||||||
|
('earnings.donations.hc.days', '0', 'HC days granted by donations earnings claims.'),
|
||||||
|
('earnings.bonus_bag.badge', '', 'Badge code granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.bonus_bag.item_id', '0', 'Items base id granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.bonus_bag.item.quantity', '1', 'Furni quantity granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.bonus_bag.hc.days', '0', 'HC days granted by bonus bag earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.badge', '', 'Badge code granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.item_id', '0', 'Items base id granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.item.quantity', '1', 'Furni quantity granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.mystery_boxes.hc.days', '0', 'HC days granted by mystery boxes earnings claims.'),
|
||||||
|
('earnings.club_job.badge', '', 'Badge code granted by club and job earnings claims.'),
|
||||||
|
('earnings.club_job.item_id', '0', 'Items base id granted by club and job earnings claims.'),
|
||||||
|
('earnings.club_job.item.quantity', '1', 'Furni quantity granted by club and job earnings claims.'),
|
||||||
|
('earnings.club_job.hc.days', '0', 'HC days granted by club and job earnings claims.');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||||
|
('earnings.daily_gift.native.enabled', '0', 'Use native hotel subsystem data for daily gift earnings claims when available.'),
|
||||||
|
('earnings.games.native.enabled', '0', 'Use native hotel subsystem data for games earnings claims when available.'),
|
||||||
|
('earnings.achievements.native.enabled', '1', 'Use achievement score thresholds for achievements earnings claims.'),
|
||||||
|
('earnings.marketplace.native.enabled', '1', 'Use marketplace sold item payouts for marketplace earnings claims.'),
|
||||||
|
('earnings.hc_payday.native.enabled', '1', 'Use unclaimed HC payday logs for HC payday earnings claims.'),
|
||||||
|
('earnings.level_progress.native.enabled', '1', 'Use talent track levels for level progress earnings claims.'),
|
||||||
|
('earnings.donations.native.enabled', '0', 'Use native hotel subsystem data for donations earnings claims when available.'),
|
||||||
|
('earnings.bonus_bag.native.enabled', '0', 'Use native hotel subsystem data for bonus bag earnings claims when available.'),
|
||||||
|
('earnings.mystery_boxes.native.enabled', '0', 'Use native hotel subsystem data for mystery boxes earnings claims when available.'),
|
||||||
|
('earnings.club_job.native.enabled', '0', 'Use native hotel subsystem data for club and job earnings claims when available.');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||||
|
('earnings.achievements.min_score', '1', 'Minimum achievement score required before achievements earnings can be claimed.'),
|
||||||
|
('earnings.achievements.score.step', '100', 'Achievement score bucket size used to prevent repeated claims for the same progress band.'),
|
||||||
|
('earnings.level_progress.min_level', '1', 'Minimum citizenship/helper talent level required before level progress earnings can be claimed.');
|
||||||
@@ -49,6 +49,7 @@ INSERT INTO `permission_definitions` (`permission_key`, `rank_7`, `comment`) VAL
|
|||||||
ON DUPLICATE KEY UPDATE `rank_7` = VALUES(`rank_7`);
|
ON DUPLICATE KEY UPDATE `rank_7` = VALUES(`rank_7`);
|
||||||
|
|
||||||
INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
||||||
|
('commands.description.cmd_setroom_template', ':setroom_template'),
|
||||||
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
|
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
|
||||||
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
|
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
|
||||||
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
||||||
|
|||||||
@@ -301,6 +301,7 @@ INSERT IGNORE INTO `custom_prefixes_catalog`
|
|||||||
|
|
||||||
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||||
-- GivePrefix command
|
-- GivePrefix command
|
||||||
|
('commands.description.cmd_give_prefix', ':giveprefix <username> <text> <color> [icon] [effect]'),
|
||||||
('commands.keys.cmd_give_prefix', 'giveprefix'),
|
('commands.keys.cmd_give_prefix', 'giveprefix'),
|
||||||
('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix <username> <text> <color> [icon] [effect]'),
|
('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix <username> <text> <color> [icon] [effect]'),
|
||||||
('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'),
|
('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'),
|
||||||
@@ -308,12 +309,14 @@ INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'),
|
('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'),
|
||||||
('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'),
|
('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'),
|
||||||
-- ListPrefixes command
|
-- ListPrefixes command
|
||||||
|
('commands.description.cmd_list_prefixes', ':listprefixes <username>'),
|
||||||
('commands.keys.cmd_list_prefixes', 'listprefixes'),
|
('commands.keys.cmd_list_prefixes', 'listprefixes'),
|
||||||
('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes <username>'),
|
('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes <username>'),
|
||||||
('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'),
|
('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'),
|
||||||
('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'),
|
('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'),
|
||||||
('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'),
|
('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'),
|
||||||
-- RemovePrefix command
|
-- RemovePrefix command
|
||||||
|
('commands.description.cmd_remove_prefix', ':removeprefix <username> <id|all>'),
|
||||||
('commands.keys.cmd_remove_prefix', 'removeprefix'),
|
('commands.keys.cmd_remove_prefix', 'removeprefix'),
|
||||||
('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix <username> <id|all>'),
|
('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix <username> <id|all>'),
|
||||||
('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'),
|
('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'),
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- 020_furnidata_edit_log.sql
|
||||||
|
-- Audit trail for furnidata name/description edits made through the furni editor,
|
||||||
|
-- plus config keys for the editor write path. NOTE: *.enabled keys elsewhere are
|
||||||
|
-- read via Boolean.parseBoolean (true/false), but these two are numeric.
|
||||||
|
CREATE TABLE IF NOT EXISTS `furnidata_edit_log` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(11) NOT NULL,
|
||||||
|
`classname` varchar(255) NOT NULL,
|
||||||
|
`action` enum('edit','revert') NOT NULL DEFAULT 'edit',
|
||||||
|
`old_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`old_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`timestamp` int(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_classname` (`classname`),
|
||||||
|
INDEX `idx_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
|
||||||
|
('items.furnidata.edit.backup.keep','10'),
|
||||||
|
('items.furnidata.edit.ratelimit.ms','2000');
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- 021_furnidata_config_cleanup.sql
|
||||||
|
-- Reverts the emulator_settings rows inserted by 021_furnidata_config.sql.
|
||||||
|
--
|
||||||
|
-- Safe default:
|
||||||
|
-- This script ends with ROLLBACK. Run it once to preview the exact rows, then
|
||||||
|
-- change the final ROLLBACK to COMMIT only if the preview is correct.
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS cleanup_furnidata_settings;
|
||||||
|
CREATE TEMPORARY TABLE cleanup_furnidata_settings (
|
||||||
|
`key` VARCHAR(255) NOT NULL PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO cleanup_furnidata_settings (`key`) VALUES
|
||||||
|
('items.furnidata.names.enabled'),
|
||||||
|
('items.furnidata.path'),
|
||||||
|
('items.furnidata.max.bytes'),
|
||||||
|
('items.furnidata.watch.enabled'),
|
||||||
|
('items.furnidata.watch.debounce.ms'),
|
||||||
|
('items.furnidata.watch.min.interval.ms'),
|
||||||
|
('items.furnidata.delta.cap'),
|
||||||
|
('furni.editor.import.url'),
|
||||||
|
('furni.editor.import.cache.ms');
|
||||||
|
|
||||||
|
-- Preview rows that will be removed.
|
||||||
|
SELECT es.`key`, es.`value`
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`
|
||||||
|
ORDER BY es.`key`;
|
||||||
|
|
||||||
|
DELETE es
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Preview remaining matching rows inside the transaction.
|
||||||
|
SELECT COUNT(*) AS remaining_furnidata_settings
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Safe default. Change to COMMIT after reviewing the preview.
|
||||||
|
ROLLBACK;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Fix NULL room paint columns
|
||||||
|
--
|
||||||
|
-- Some legacy/imported rooms have NULL in paper_wall / paper_floor / paper_landscape.
|
||||||
|
-- The server compares these with .equals("0.0") on room entry, which throws a
|
||||||
|
-- NullPointerException (RoomManager.openRoom) and prevents the room from loading.
|
||||||
|
-- This normalizes existing NULL values and re-enforces the NOT NULL DEFAULT '0.0'
|
||||||
|
-- constraint so it cannot happen again.
|
||||||
|
|
||||||
|
UPDATE `rooms` SET `paper_wall` = '0.0' WHERE `paper_wall` IS NULL;
|
||||||
|
UPDATE `rooms` SET `paper_floor` = '0.0' WHERE `paper_floor` IS NULL;
|
||||||
|
UPDATE `rooms` SET `paper_landscape` = '0.0' WHERE `paper_landscape` IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE `rooms` MODIFY COLUMN `paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0';
|
||||||
|
ALTER TABLE `rooms` MODIFY COLUMN `paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0';
|
||||||
|
ALTER TABLE `rooms` MODIFY COLUMN `paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0';
|
||||||
@@ -15355,7 +15355,9 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.cmd_promote_offer.list', 'All available offers (%amount%):<br>%list%'),
|
('commands.cmd_promote_offer.list', 'All available offers (%amount%):<br>%list%'),
|
||||||
('commands.cmd_promote_offer.list.entry', '%id%: %title% %description%'),
|
('commands.cmd_promote_offer.list.entry', '%id%: %title% %description%'),
|
||||||
('commands.description.acc_debug', ':test [header] i:1 s:a b:1'),
|
('commands.description.acc_debug', ':test [header] i:1 s:a b:1'),
|
||||||
|
('commands.description.acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'),
|
||||||
('commands.description.cmd_about', ':about'),
|
('commands.description.cmd_about', ':about'),
|
||||||
|
('commands.description.cmd_add_youtube_playlist', ':add_youtube <base_item_id> <youtube_playlist_id>'),
|
||||||
('commands.description.cmd_alert', ':alert <username> <message>'),
|
('commands.description.cmd_alert', ':alert <username> <message>'),
|
||||||
('commands.description.cmd_allow_trading', 'Enables / Disables the tradelock for a user.'),
|
('commands.description.cmd_allow_trading', 'Enables / Disables the tradelock for a user.'),
|
||||||
('commands.description.cmd_badge', ':badge <username> <badge>'),
|
('commands.description.cmd_badge', ':badge <username> <badge>'),
|
||||||
@@ -15379,6 +15381,8 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.description.cmd_danceall', ':danceall <dance id>'),
|
('commands.description.cmd_danceall', ':danceall <dance id>'),
|
||||||
('commands.description.cmd_diagonal', ':diagonal'),
|
('commands.description.cmd_diagonal', ':diagonal'),
|
||||||
('commands.description.cmd_disable_effects', ':disableffects'),
|
('commands.description.cmd_disable_effects', ':disableffects'),
|
||||||
|
('commands.description.cmd_disablemassmentions', ':disablemassmentions'),
|
||||||
|
('commands.description.cmd_disablementions', ':disablementions'),
|
||||||
('commands.description.cmd_disconnect', ':disconnect <username>'),
|
('commands.description.cmd_disconnect', ':disconnect <username>'),
|
||||||
('commands.description.cmd_duckets', ':duckets <username> <amount>'),
|
('commands.description.cmd_duckets', ':duckets <username> <amount>'),
|
||||||
('commands.description.cmd_ejectall', ':ejectall'),
|
('commands.description.cmd_ejectall', ':ejectall'),
|
||||||
@@ -15395,11 +15399,13 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.description.cmd_freeze_bots', ':freezebots'),
|
('commands.description.cmd_freeze_bots', ':freezebots'),
|
||||||
('commands.description.cmd_furnidata', ':furnidata'),
|
('commands.description.cmd_furnidata', ':furnidata'),
|
||||||
('commands.description.cmd_gift', ':gift <username> <itemid>'),
|
('commands.description.cmd_gift', ':gift <username> <itemid>'),
|
||||||
|
('commands.description.cmd_give_prefix', ':giveprefix <username> <text> <color> [icon] [effect]'),
|
||||||
('commands.description.cmd_give_rank', ':giverank <username> <rank>'),
|
('commands.description.cmd_give_rank', ':giverank <username> <rank>'),
|
||||||
('commands.description.cmd_ha', ':ha <message>'),
|
('commands.description.cmd_ha', ':ha <message>'),
|
||||||
('commands.description.cmd_hal', ':hal <url> <message>'),
|
('commands.description.cmd_hal', ':hal <url> <message>'),
|
||||||
('commands.description.cmd_hand_item', ':handitem <itemid>'),
|
('commands.description.cmd_hand_item', ':handitem <itemid>'),
|
||||||
('commands.description.cmd_happyhour', ':happyhour'),
|
('commands.description.cmd_happyhour', ':happyhour'),
|
||||||
|
('commands.description.cmd_hidewired', ':hidewired'),
|
||||||
('commands.description.cmd_hoverboard', ':hoverboard'),
|
('commands.description.cmd_hoverboard', ':hoverboard'),
|
||||||
('commands.description.cmd_hug', ':hug <username>'),
|
('commands.description.cmd_hug', ':hug <username>'),
|
||||||
('commands.description.cmd_invisible', ':invisible'),
|
('commands.description.cmd_invisible', ':invisible'),
|
||||||
@@ -15408,6 +15414,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.description.cmd_kill', ':kill <username>'),
|
('commands.description.cmd_kill', ':kill <username>'),
|
||||||
('commands.description.cmd_kiss', ':kiss <username>'),
|
('commands.description.cmd_kiss', ':kiss <username>'),
|
||||||
('commands.description.cmd_lay', ':lay'),
|
('commands.description.cmd_lay', ':lay'),
|
||||||
|
('commands.description.cmd_list_prefixes', ':listprefixes <username>'),
|
||||||
('commands.description.cmd_machine_ban', ':machineban <username> [reason]'),
|
('commands.description.cmd_machine_ban', ':machineban <username> [reason]'),
|
||||||
('commands.description.cmd_massbadge', ':massbadge <badge>'),
|
('commands.description.cmd_massbadge', ':massbadge <badge>'),
|
||||||
('commands.description.cmd_masscredits', ':masscredits <amount>'),
|
('commands.description.cmd_masscredits', ':masscredits <amount>'),
|
||||||
@@ -15429,6 +15436,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.description.cmd_push', ':push <username>'),
|
('commands.description.cmd_push', ':push <username>'),
|
||||||
('commands.description.cmd_redeem', ':redeem'),
|
('commands.description.cmd_redeem', ':redeem'),
|
||||||
('commands.description.cmd_reload_room', ':reload_room'),
|
('commands.description.cmd_reload_room', ':reload_room'),
|
||||||
|
('commands.description.cmd_remove_prefix', ':removeprefix <username> <id|all>'),
|
||||||
('commands.description.cmd_roomalert', ':roomalert <message>'),
|
('commands.description.cmd_roomalert', ':roomalert <message>'),
|
||||||
('commands.description.cmd_roombadge', ':roombadge <badge>'),
|
('commands.description.cmd_roombadge', ':roombadge <badge>'),
|
||||||
('commands.description.cmd_roomcredits', ':roomcredits <amount>'),
|
('commands.description.cmd_roomcredits', ':roomcredits <amount>'),
|
||||||
@@ -15444,6 +15452,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.description.cmd_set', ':set info'),
|
('commands.description.cmd_set', ':set info'),
|
||||||
('commands.description.cmd_setmax', ':setmax <amount>'),
|
('commands.description.cmd_setmax', ':setmax <amount>'),
|
||||||
('commands.description.cmd_setpublic', ':setpublic'),
|
('commands.description.cmd_setpublic', ':setpublic'),
|
||||||
|
('commands.description.cmd_setroom_template', ':setroom_template'),
|
||||||
('commands.description.cmd_setrotation', ':rot;rotation'),
|
('commands.description.cmd_setrotation', ':rot;rotation'),
|
||||||
('commands.description.cmd_setspeed', ':setspeed <speed>'),
|
('commands.description.cmd_setspeed', ':setspeed <speed>'),
|
||||||
('commands.description.cmd_setstate', ':ss'),
|
('commands.description.cmd_setstate', ':ss'),
|
||||||
@@ -15486,6 +15495,7 @@ INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
|||||||
('commands.description.cmd_update_polls', ':update_polls'),
|
('commands.description.cmd_update_polls', ':update_polls'),
|
||||||
('commands.description.cmd_update_texts', ':update_texts'),
|
('commands.description.cmd_update_texts', ':update_texts'),
|
||||||
('commands.description.cmd_update_wordfilter', ':update_word_filter'),
|
('commands.description.cmd_update_wordfilter', ':update_word_filter'),
|
||||||
|
('commands.description.cmd_update_youtube_playlists', ':update_youtube'),
|
||||||
('commands.description.cmd_userinfo', ':userinfo <username>'),
|
('commands.description.cmd_userinfo', ':userinfo <username>'),
|
||||||
('commands.description.cmd_welcome', ':welcome <username>'),
|
('commands.description.cmd_welcome', ':welcome <username>'),
|
||||||
('commands.description.cmd_word_quiz', ':wordquiz <question>'),
|
('commands.description.cmd_word_quiz', ':wordquiz <question>'),
|
||||||
|
|||||||
+57
-19
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>com.eu.habbo</groupId>
|
<groupId>com.eu.habbo</groupId>
|
||||||
<artifactId>Habbo</artifactId>
|
<artifactId>Habbo</artifactId>
|
||||||
<version>4.2.34</version>
|
<version>4.2.44</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
@@ -62,6 +62,12 @@
|
|||||||
<show>public</show>
|
<show>public</show>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.2</version>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
@@ -77,21 +83,21 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.netty</groupId>
|
<groupId>io.netty</groupId>
|
||||||
<artifactId>netty-all</artifactId>
|
<artifactId>netty-all</artifactId>
|
||||||
<version>4.1.115.Final</version>
|
<version>4.2.15.Final</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- GSON -->
|
<!-- GSON -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>2.11.0</version>
|
<version>2.14.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- MariaDB Connector/J (native driver for MariaDB) -->
|
<!-- MariaDB Connector/J (native driver for MariaDB) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mariadb.jdbc</groupId>
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
<artifactId>mariadb-java-client</artifactId>
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
<version>3.5.1</version>
|
<version>3.5.8</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -107,7 +113,38 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.zaxxer</groupId>
|
<groupId>com.zaxxer</groupId>
|
||||||
<artifactId>HikariCP</artifactId>
|
<artifactId>HikariCP</artifactId>
|
||||||
<version>6.2.1</version>
|
<version>7.0.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Caffeine cache - high-performance local caching for hot emulator lookups -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>3.2.4</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Resilience4j - rate limits and circuit breakers for RCON/HTTP/external integrations -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-ratelimiter</artifactId>
|
||||||
|
<version>2.4.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-circuitbreaker</artifactId>
|
||||||
|
<version>2.4.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Hibernate Validator - Jakarta Bean Validation for packet/RCON/admin DTO guards -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate.validator</groupId>
|
||||||
|
<artifactId>hibernate-validator</artifactId>
|
||||||
|
<version>9.1.0.Final</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -115,7 +152,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-lang3</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
<version>3.17.0</version>
|
<version>3.20.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -131,7 +168,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
<version>1.18.3</version>
|
<version>1.22.2</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -139,14 +176,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
<version>2.0.16</version>
|
<version>2.0.18</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Logback -->
|
<!-- Logback -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>ch.qos.logback</groupId>
|
||||||
<artifactId>logback-classic</artifactId>
|
<artifactId>logback-classic</artifactId>
|
||||||
<version>1.5.15</version>
|
<version>1.5.34</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -154,14 +191,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.fusesource.jansi</groupId>
|
<groupId>org.fusesource.jansi</groupId>
|
||||||
<artifactId>jansi</artifactId>
|
<artifactId>jansi</artifactId>
|
||||||
<version>2.4.1</version>
|
<version>2.4.3</version>
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Joda Time -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>joda-time</groupId>
|
|
||||||
<artifactId>joda-time</artifactId>
|
|
||||||
<version>2.13.0</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- jBCrypt � used by the built-in /api/auth/* HTTP login handler
|
<!-- jBCrypt � used by the built-in /api/auth/* HTTP login handler
|
||||||
@@ -172,12 +202,20 @@
|
|||||||
<version>0.4</version>
|
<version>0.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Jakarta Mail � used by the built-in forgot-password endpoint
|
<!-- Jakarta Mail — used by the built-in forgot-password endpoint
|
||||||
when smtp.* keys are configured in emulator_settings -->
|
when smtp.* keys are configured in emulator_settings -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.angus</groupId>
|
<groupId>org.eclipse.angus</groupId>
|
||||||
<artifactId>jakarta.mail</artifactId>
|
<artifactId>jakarta.mail</artifactId>
|
||||||
<version>2.0.3</version>
|
<version>2.0.5</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JUnit Jupiter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>6.1.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import com.eu.habbo.plugin.events.emulator.EmulatorStartShutdownEvent;
|
|||||||
import com.eu.habbo.plugin.events.emulator.EmulatorStoppedEvent;
|
import com.eu.habbo.plugin.events.emulator.EmulatorStoppedEvent;
|
||||||
import com.eu.habbo.threading.ThreadPooling;
|
import com.eu.habbo.threading.ThreadPooling;
|
||||||
import com.eu.habbo.util.imager.badges.BadgeImager;
|
import com.eu.habbo.util.imager.badges.BadgeImager;
|
||||||
|
import com.eu.habbo.util.logback.ConsoleStyle;
|
||||||
|
import org.fusesource.jansi.AnsiConsole;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -38,6 +40,12 @@ public final class Emulator {
|
|||||||
private static final Logger LOGGER = LoggerFactory.getLogger(Emulator.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(Emulator.class);
|
||||||
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
|
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
|
||||||
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
|
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
|
||||||
|
private static final String ANSI_RESET = "\u001B[0m";
|
||||||
|
private static final String ANSI_BOLD = "\u001B[1m";
|
||||||
|
private static final String ANSI_CYAN = "\u001B[36m";
|
||||||
|
private static final String ANSI_GREEN = "\u001B[32m";
|
||||||
|
private static final String ANSI_YELLOW = "\u001B[33m";
|
||||||
|
private static final String ANSI_DIM = "\u001B[2m";
|
||||||
|
|
||||||
// Fallback version, only used when running outside a packaged jar (e.g. from
|
// Fallback version, only used when running outside a packaged jar (e.g. from
|
||||||
// the IDE). In production the version comes from the jar manifest below.
|
// the IDE). In production the version comes from the jar manifest below.
|
||||||
@@ -65,7 +73,6 @@ public final class Emulator {
|
|||||||
"██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝███████║ ██║ ██║ ██║██║ ██║\n" +
|
"██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝███████║ ██║ ██║ ██║██║ ██║\n" +
|
||||||
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝\n" +
|
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝\n" +
|
||||||
"Still Rocking in 2026.\n";
|
"Still Rocking in 2026.\n";
|
||||||
|
|
||||||
public static String build = "";
|
public static String build = "";
|
||||||
public static long buildTimestamp = -1L;
|
public static long buildTimestamp = -1L;
|
||||||
|
|
||||||
@@ -104,14 +111,12 @@ public final class Emulator {
|
|||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
try {
|
try {
|
||||||
if (OS_NAME.startsWith("Windows") && !CLASS_PATH.contains("idea_rt.jar")) {
|
boolean styledConsole = shouldStyleConsole(
|
||||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
System.getenv(),
|
||||||
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console");
|
System.console() != null,
|
||||||
|
OS_NAME,
|
||||||
appender.stop();
|
System.getProperty("habbo.console.style", "auto"));
|
||||||
appender.setWithJansi(true);
|
configureAnsiConsole(styledConsole);
|
||||||
appender.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
Locale.setDefault(Locale.of("en"));
|
Locale.setDefault(Locale.of("en"));
|
||||||
setBuild();
|
setBuild();
|
||||||
@@ -119,7 +124,7 @@ public final class Emulator {
|
|||||||
ConsoleCommand.load();
|
ConsoleCommand.load();
|
||||||
Emulator.logging = new Logging();
|
Emulator.logging = new Logging();
|
||||||
|
|
||||||
System.out.println(logo);
|
System.out.println(startupHero(styledConsole));
|
||||||
|
|
||||||
long startTime = System.nanoTime();
|
long startTime = System.nanoTime();
|
||||||
|
|
||||||
@@ -153,14 +158,21 @@ public final class Emulator {
|
|||||||
Emulator.config.register("camera.price.points.type", "5");
|
Emulator.config.register("camera.price.points.type", "5");
|
||||||
Emulator.config.register("camera.render.delay", "5");
|
Emulator.config.register("camera.render.delay", "5");
|
||||||
Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||||
|
Emulator.config.register("gui.enabled", "0");
|
||||||
|
Emulator.config.register("gui.autostart.enabled", "0");
|
||||||
|
Emulator.config.register("rcon.rate_limit.enabled", "1");
|
||||||
|
Emulator.config.register("rcon.rate_limit.limit_for_period", "60");
|
||||||
|
Emulator.config.register("rcon.rate_limit.refresh_period_ms", "1000");
|
||||||
|
Emulator.config.register("rcon.rate_limit.timeout_ms", "0");
|
||||||
|
Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank");
|
||||||
|
Emulator.config.register("rcon.execute_command.allowed_permissions", "");
|
||||||
|
Emulator.config.register("rcon.max_payload_bytes", "65536");
|
||||||
|
Emulator.config.register("nitro.secure.api.max_payload_bytes", "65536");
|
||||||
|
Emulator.config.register("nitro.secure.config.max_file_bytes", "2097152");
|
||||||
|
Emulator.config.register("nitro.secure.gamedata.max_file_bytes", "16777216");
|
||||||
|
registerEarningsSettings();
|
||||||
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||||
System.out.println();
|
System.out.println(startupCard(hotelTimezoneId));
|
||||||
LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, ");
|
|
||||||
System.out.println();
|
|
||||||
LOGGER.info("This project is for educational purposes only. This Emulator is an open-source fork of Arcturus created by TheGeneral.");
|
|
||||||
LOGGER.info("Version: {}", version);
|
|
||||||
LOGGER.info("Build: {}", build);
|
|
||||||
LOGGER.info("Build Timestamp: {} [{}]", formatBuildTimestamp(buildTimestamp, hotelTimezoneId), hotelTimezoneId);
|
|
||||||
Emulator.texts.register("camera.permission", "You don't have permission to use the camera!");
|
Emulator.texts.register("camera.permission", "You don't have permission to use the camera!");
|
||||||
Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture.");
|
Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture.");
|
||||||
Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*");
|
Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*");
|
||||||
@@ -198,7 +210,7 @@ public final class Emulator {
|
|||||||
Emulator.isReady = true;
|
Emulator.isReady = true;
|
||||||
Emulator.timeStarted = getIntUnixTimestamp();
|
Emulator.timeStarted = getIntUnixTimestamp();
|
||||||
|
|
||||||
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
|
if (shouldLaunchGui()) {
|
||||||
EmulatorDashboard.launch();
|
EmulatorDashboard.launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +322,97 @@ public final class Emulator {
|
|||||||
return -1L;
|
return -1L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String startupCard(String hotelTimezoneId) {
|
||||||
|
return "\n" +
|
||||||
|
"+----------------------------------------------------------------+\n" +
|
||||||
|
"| Arcturus Morningstar Extended |\n" +
|
||||||
|
"| Source : github.com/duckietm/Arcturus-Morningstar-Extended |\n" +
|
||||||
|
"| Scope : Educational open-source fork by TheGeneral |\n" +
|
||||||
|
"| Version: " + version + "\n" +
|
||||||
|
"| Build : " + build + "\n" +
|
||||||
|
"| Time : " + formatBuildTimestamp(buildTimestamp, hotelTimezoneId) + " [" + hotelTimezoneId + "]\n" +
|
||||||
|
"+----------------------------------------------------------------+\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String startupHero() {
|
||||||
|
return startupHero(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String startupHero(boolean styled) {
|
||||||
|
if (styled) {
|
||||||
|
return "\n" +
|
||||||
|
ANSI_CYAN +
|
||||||
|
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
|
||||||
|
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
|
||||||
|
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
|
||||||
|
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
|
||||||
|
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
|
||||||
|
ANSI_RESET +
|
||||||
|
"\n" +
|
||||||
|
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
|
||||||
|
"| " + ANSI_BOLD + ANSI_GREEN + "[OK] MORNINGSTAR EXTENDED" + ANSI_RESET + fit("", 50) + " |\n" +
|
||||||
|
"| " + ANSI_DIM + "Arcturus game server runtime" + ANSI_RESET + fit("", 48) + " |\n" +
|
||||||
|
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
|
||||||
|
"| " + ANSI_YELLOW + "[VER]" + ANSI_RESET + " Version : " + fit(version, 57) + " |\n" +
|
||||||
|
"| " + ANSI_YELLOW + "[BLD]" + ANSI_RESET + " Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 57) + " |\n" +
|
||||||
|
"| " + ANSI_YELLOW + "[JVM]" + ANSI_RESET + " Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / styled console output", 57) + " |\n" +
|
||||||
|
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\n" +
|
||||||
|
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
|
||||||
|
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
|
||||||
|
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
|
||||||
|
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
|
||||||
|
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
|
||||||
|
"\n" +
|
||||||
|
"+------------------------------------------------------------------------------+\n" +
|
||||||
|
"| MORNINGSTAR EXTENDED |\n" +
|
||||||
|
"| Arcturus game server runtime |\n" +
|
||||||
|
"+------------------------------------------------------------------------------+\n" +
|
||||||
|
"| Version : " + fit(version, 63) + " |\n" +
|
||||||
|
"| Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 63) + " |\n" +
|
||||||
|
"| Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / universal console output", 63) + " |\n" +
|
||||||
|
"+------------------------------------------------------------------------------+\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean shouldStyleConsole(Map<String, String> environment, boolean interactiveConsole, String osName, String styleProperty) {
|
||||||
|
return ConsoleStyle.isEnabled(environment, interactiveConsole, osName, styleProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void configureAnsiConsole(boolean styledConsole) {
|
||||||
|
if (!styledConsole || !OS_NAME.startsWith("Windows") || CLASS_PATH.contains("idea_rt.jar")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
AnsiConsole.systemInstall();
|
||||||
|
|
||||||
|
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||||
|
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console");
|
||||||
|
if (appender != null) {
|
||||||
|
appender.stop();
|
||||||
|
appender.setWithJansi(true);
|
||||||
|
appender.start();
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.debug("Unable to install Jansi console bridge; continuing with raw console output.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean shouldLaunchGui() {
|
||||||
|
return Emulator.getConfig() != null && Emulator.getConfig().getBoolean("gui.autostart.enabled", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fit(String value, int width) {
|
||||||
|
String safe = value == null ? "" : value;
|
||||||
|
if (safe.length() > width) {
|
||||||
|
return safe.substring(0, Math.max(0, width - 3)) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.format("%-" + width + "s", safe);
|
||||||
|
}
|
||||||
|
|
||||||
private static String formatBuildTimestamp(long buildTimestamp, String timezoneId) {
|
private static String formatBuildTimestamp(long buildTimestamp, String timezoneId) {
|
||||||
if (buildTimestamp <= 0) {
|
if (buildTimestamp <= 0) {
|
||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
@@ -390,6 +493,42 @@ public final class Emulator {
|
|||||||
return gameServer;
|
return gameServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void registerEarningsSettings() {
|
||||||
|
Emulator.config.register("earnings.enabled", "0");
|
||||||
|
|
||||||
|
String[] categories = {
|
||||||
|
"daily_gift",
|
||||||
|
"games",
|
||||||
|
"achievements",
|
||||||
|
"marketplace",
|
||||||
|
"hc_payday",
|
||||||
|
"level_progress",
|
||||||
|
"donations",
|
||||||
|
"bonus_bag",
|
||||||
|
"mystery_boxes",
|
||||||
|
"club_job"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String category : categories) {
|
||||||
|
String prefix = "earnings." + category + ".";
|
||||||
|
Emulator.config.register(prefix + "enabled", "1");
|
||||||
|
Emulator.config.register(prefix + "cooldown.seconds", "86400");
|
||||||
|
Emulator.config.register(prefix + "credits", "0");
|
||||||
|
Emulator.config.register(prefix + "pixels", "0");
|
||||||
|
Emulator.config.register(prefix + "points", "0");
|
||||||
|
Emulator.config.register(prefix + "points.type", "5");
|
||||||
|
Emulator.config.register(prefix + "badge", "");
|
||||||
|
Emulator.config.register(prefix + "item_id", "0");
|
||||||
|
Emulator.config.register(prefix + "item.quantity", "1");
|
||||||
|
Emulator.config.register(prefix + "hc.days", "0");
|
||||||
|
Emulator.config.register(prefix + "native.enabled", (category.equals("marketplace") || category.equals("hc_payday") || category.equals("achievements") || category.equals("level_progress")) ? "1" : "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
Emulator.config.register("earnings.achievements.min_score", "1");
|
||||||
|
Emulator.config.register("earnings.achievements.score.step", "100");
|
||||||
|
Emulator.config.register("earnings.level_progress.min_level", "1");
|
||||||
|
}
|
||||||
|
|
||||||
public static RCONServer getRconServer() {
|
public static RCONServer getRconServer() {
|
||||||
return rconServer;
|
return rconServer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ public class TextsManager {
|
|||||||
return this.texts.getProperty(key, defaultValue);
|
return this.texts.getProperty(key, defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getValueQuietly(String key, String defaultValue) {
|
||||||
|
return this.texts.getProperty(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getBoolean(String key) {
|
public boolean getBoolean(String key) {
|
||||||
return this.getBoolean(key, false);
|
return this.getBoolean(key, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ class DatabasePool {
|
|||||||
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
|
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
|
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
|
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
|
||||||
|
|
||||||
|
// Fail fast instead of pinning a pooled connection (and its worker
|
||||||
|
// thread) indefinitely on a stalled/slow MariaDB. HikariCP's
|
||||||
|
// connectionTimeout only bounds the pool *borrow*; these bound the
|
||||||
|
// actual socket/connect round-trip. Overridable via db.params.
|
||||||
|
databaseConfiguration.addDataSourceProperty("socketTimeout", "30000");
|
||||||
|
databaseConfiguration.addDataSourceProperty("connectTimeout", "10000");
|
||||||
|
databaseConfiguration.addDataSourceProperty("tcpKeepAlive", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
|
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
|
||||||
|
|
||||||
databaseConfiguration.setPoolName("HabboHikariPool");
|
databaseConfiguration.setPoolName("HabboHikariPool");
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.eu.habbo.habbohotel.crafting.CraftingManager;
|
|||||||
import com.eu.habbo.habbohotel.guides.GuideManager;
|
import com.eu.habbo.habbohotel.guides.GuideManager;
|
||||||
import com.eu.habbo.habbohotel.guilds.GuildManager;
|
import com.eu.habbo.habbohotel.guilds.GuildManager;
|
||||||
import com.eu.habbo.habbohotel.hotelview.HotelViewManager;
|
import com.eu.habbo.habbohotel.hotelview.HotelViewManager;
|
||||||
|
import com.eu.habbo.habbohotel.items.FurnitureTextProvider;
|
||||||
import com.eu.habbo.habbohotel.items.ItemManager;
|
import com.eu.habbo.habbohotel.items.ItemManager;
|
||||||
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
||||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||||
@@ -47,6 +48,7 @@ public class GameEnvironment {
|
|||||||
private NavigatorManager navigatorManager;
|
private NavigatorManager navigatorManager;
|
||||||
private GuildManager guildManager;
|
private GuildManager guildManager;
|
||||||
private ItemManager itemManager;
|
private ItemManager itemManager;
|
||||||
|
private FurnitureTextProvider furnitureTextProvider;
|
||||||
private CatalogManager catalogManager;
|
private CatalogManager catalogManager;
|
||||||
private HotelViewManager hotelViewManager;
|
private HotelViewManager hotelViewManager;
|
||||||
private RoomManager roomManager;
|
private RoomManager roomManager;
|
||||||
@@ -79,6 +81,8 @@ public class GameEnvironment {
|
|||||||
this.hotelViewManager = new HotelViewManager();
|
this.hotelViewManager = new HotelViewManager();
|
||||||
this.itemManager = new ItemManager();
|
this.itemManager = new ItemManager();
|
||||||
this.itemManager.load();
|
this.itemManager.load();
|
||||||
|
this.furnitureTextProvider = new FurnitureTextProvider();
|
||||||
|
this.furnitureTextProvider.init();
|
||||||
this.botManager = new BotManager();
|
this.botManager = new BotManager();
|
||||||
this.petManager = new PetManager();
|
this.petManager = new PetManager();
|
||||||
this.guildManager = new GuildManager();
|
this.guildManager = new GuildManager();
|
||||||
@@ -161,6 +165,10 @@ public class GameEnvironment {
|
|||||||
return this.itemManager;
|
return this.itemManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FurnitureTextProvider getFurnitureTextProvider() {
|
||||||
|
return this.furnitureTextProvider;
|
||||||
|
}
|
||||||
|
|
||||||
public CatalogManager getCatalogManager() {
|
public CatalogManager getCatalogManager() {
|
||||||
return this.catalogManager;
|
return this.catalogManager;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ public class AchievementManager {
|
|||||||
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
|
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
habbo.getHabboStats().setProgress(achievement, currentProgress + amount);
|
int newProgress = habbo.getHabboStats().incrementProgress(achievement, amount);
|
||||||
|
|
||||||
AchievementLevel newLevel = achievement.getLevelForProgress(currentProgress + amount);
|
AchievementLevel newLevel = achievement.getLevelForProgress(newProgress);
|
||||||
|
|
||||||
if (AchievementManager.TALENTTRACK_ENABLED) {
|
if (AchievementManager.TALENTTRACK_ENABLED) {
|
||||||
for (TalentTrackType type : TalentTrackType.values()) {
|
for (TalentTrackType type : TalentTrackType.values()) {
|
||||||
|
|||||||
@@ -179,23 +179,31 @@ public class BotManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void pickUpBot(Bot bot, Habbo habbo) {
|
public void pickUpBot(Bot bot, Habbo habbo) {
|
||||||
HabboInfo receiverInfo = habbo == null ? Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId()) : habbo.getHabboInfo();
|
|
||||||
|
|
||||||
if (bot != null) {
|
if (bot != null) {
|
||||||
|
HabboInfo receiverInfo = resolvePickupReceiver(bot, habbo);
|
||||||
|
Room botRoom = bot.getRoom();
|
||||||
|
if (receiverInfo == null || botRoom == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
BotPickUpEvent pickedUpEvent = new BotPickUpEvent(bot, habbo);
|
BotPickUpEvent pickedUpEvent = new BotPickUpEvent(bot, habbo);
|
||||||
Emulator.getPluginManager().fireEvent(pickedUpEvent);
|
Emulator.getPluginManager().fireEvent(pickedUpEvent);
|
||||||
|
|
||||||
if (pickedUpEvent.isCancelled())
|
if (pickedUpEvent.isCancelled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (habbo == null || (bot.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_ANYROOMOWNER))) {
|
Room currentRoom = habbo != null ? habbo.getHabboInfo().getCurrentRoom() : null;
|
||||||
|
if (habbo == null
|
||||||
|
|| bot.getOwnerId() == habbo.getHabboInfo().getId()
|
||||||
|
|| habbo.hasPermission(Permission.ACC_ANYROOMOWNER)
|
||||||
|
|| (currentRoom != null && (currentRoom.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_PLACEFURNI)))) {
|
||||||
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.onPickUp(habbo, receiverInfo.getCurrentRoom());
|
bot.onPickUp(habbo, botRoom);
|
||||||
receiverInfo.getCurrentRoom().removeBot(bot);
|
botRoom.removeBot(bot);
|
||||||
bot.stopFollowingHabbo();
|
bot.stopFollowingHabbo();
|
||||||
bot.setOwnerId(receiverInfo.getId());
|
bot.setOwnerId(receiverInfo.getId());
|
||||||
bot.setOwnerName(receiverInfo.getUsername());
|
bot.setOwnerName(receiverInfo.getUsername());
|
||||||
@@ -211,6 +219,14 @@ public class BotManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HabboInfo resolvePickupReceiver(Bot bot, Habbo picker) {
|
||||||
|
if (picker != null && bot.getOwnerId() == picker.getHabboInfo().getId()) {
|
||||||
|
return picker.getHabboInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId());
|
||||||
|
}
|
||||||
|
|
||||||
public Bot loadBot(ResultSet set) {
|
public Bot loadBot(ResultSet set) {
|
||||||
try {
|
try {
|
||||||
String type = set.getString("type");
|
String type = set.getString("type");
|
||||||
|
|||||||
@@ -174,6 +174,14 @@ public class CatalogItem implements ISerialize, Runnable, Comparable<CatalogItem
|
|||||||
return this.offerId;
|
return this.offerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSearchOfferId() {
|
||||||
|
if (this.offerId > 0) {
|
||||||
|
return this.offerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return haveOffer(this) ? this.id : -1;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isLimited() {
|
public boolean isLimited() {
|
||||||
return this.limitedStack > 0;
|
return this.limitedStack > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,10 +494,11 @@ public class CatalogManager {
|
|||||||
item = new CatalogItem(set);
|
item = new CatalogItem(set);
|
||||||
page.addItem(item);
|
page.addItem(item);
|
||||||
|
|
||||||
if (item.getOfferId() != -1) {
|
int searchOfferId = item.getSearchOfferId();
|
||||||
page.addOfferId(item.getOfferId());
|
if (searchOfferId != -1) {
|
||||||
|
page.addOfferId(searchOfferId);
|
||||||
|
|
||||||
this.offerDefs.put(item.getOfferId(), item.getId());
|
this.offerDefs.put(searchOfferId, item.getId());
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
item.update(set);
|
item.update(set);
|
||||||
@@ -711,18 +712,22 @@ public class CatalogManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (voucher.isExhausted()) {
|
Voucher.ClaimResult claimResult = voucher.claimForUser(habbo.getHabboInfo().getId());
|
||||||
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR));
|
switch (claimResult) {
|
||||||
return;
|
case CLAIMED:
|
||||||
|
break;
|
||||||
|
case EXHAUSTED:
|
||||||
|
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR));
|
||||||
|
return;
|
||||||
|
case USER_LIMIT:
|
||||||
|
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
|
||||||
|
return;
|
||||||
|
case FAILED:
|
||||||
|
default:
|
||||||
|
client.sendResponse(new RedeemVoucherErrorComposer(RedeemVoucherErrorComposer.TECHNICAL_ERROR));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (voucher.hasUserExhausted(habbo.getHabboInfo().getId())) {
|
|
||||||
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
voucher.addHistoryEntry(habbo.getHabboInfo().getId());
|
|
||||||
|
|
||||||
if (voucher.points > 0) {
|
if (voucher.points > 0) {
|
||||||
client.getHabbo().givePoints(voucher.pointsType, voucher.points);
|
client.getHabbo().givePoints(voucher.pointsType, voucher.points);
|
||||||
}
|
}
|
||||||
@@ -1054,13 +1059,13 @@ public class CatalogManager {
|
|||||||
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
||||||
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
||||||
if (habbo.getHabboStats().totalLtds() >= ltdLimit) {
|
if (habbo.getHabboStats().totalLtds() >= ltdLimit) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getDisplayName()).replace("%limit%", ltdLimit + ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
||||||
if (habbo.getHabboStats().totalLtds(item.id) >= ltdLimit) {
|
if (habbo.getHabboStats().totalLtds(item.id) >= ltdLimit) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getDisplayName()).replace("%limit%", ltdLimit + ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1247,6 +1252,11 @@ public class CatalogManager {
|
|||||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||||
|
|
||||||
if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) {
|
if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) {
|
||||||
|
if (baseItem.getName().equals("guild_forum") && guild.getOwnerId() != habbo.getHabboInfo().getId()) {
|
||||||
|
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata);
|
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata);
|
||||||
habboItem.setExtradata("");
|
habboItem.setExtradata("");
|
||||||
habboItem.needsUpdate(true);
|
habboItem.needsUpdate(true);
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import java.util.List;
|
|||||||
public class Voucher {
|
public class Voucher {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class);
|
||||||
|
|
||||||
|
public enum ClaimResult {
|
||||||
|
CLAIMED,
|
||||||
|
EXHAUSTED,
|
||||||
|
USER_LIMIT,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
public final int id;
|
public final int id;
|
||||||
public final String code;
|
public final String code;
|
||||||
public final int credits;
|
public final int credits;
|
||||||
@@ -58,18 +65,34 @@ public class Voucher {
|
|||||||
return this.amount > 0 && this.history.size() >= this.amount;
|
return this.amount > 0 && this.history.size() >= this.amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addHistoryEntry(int userId) {
|
public synchronized ClaimResult claimForUser(int userId) {
|
||||||
int timestamp = Emulator.getIntUnixTimestamp();
|
if (this.isExhausted()) {
|
||||||
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp));
|
return ClaimResult.EXHAUSTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasUserExhausted(userId)) {
|
||||||
|
return ClaimResult.USER_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int timestamp = Emulator.getIntUnixTimestamp();
|
||||||
|
if (!this.insertHistoryEntry(userId, timestamp)) {
|
||||||
|
return ClaimResult.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp));
|
||||||
|
return ClaimResult.CLAIMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean insertHistoryEntry(int userId, int timestamp) {
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) {
|
||||||
statement.setInt(1, this.id);
|
statement.setInt(1, this.id);
|
||||||
statement.setInt(2, userId);
|
statement.setInt(2, userId);
|
||||||
statement.setInt(3, timestamp);
|
statement.setInt(3, timestamp);
|
||||||
|
|
||||||
statement.execute();
|
return statement.executeUpdate() > 0;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-15
@@ -28,6 +28,8 @@ import java.util.List;
|
|||||||
|
|
||||||
public class MarketPlace {
|
public class MarketPlace {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(MarketPlace.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(MarketPlace.class);
|
||||||
|
public static final int MINIMUM_LISTING_PRICE = 1;
|
||||||
|
public static final int MAXIMUM_LISTING_PRICE = 1_000_000_000;
|
||||||
|
|
||||||
//Configuration. Loaded from database & updated accordingly.
|
//Configuration. Loaded from database & updated accordingly.
|
||||||
public static boolean MARKETPLACE_ENABLED = true;
|
public static boolean MARKETPLACE_ENABLED = true;
|
||||||
@@ -56,6 +58,10 @@ public class MarketPlace {
|
|||||||
public static void takeBackItem(Habbo habbo, int offerId) {
|
public static void takeBackItem(Habbo habbo, int offerId) {
|
||||||
MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
|
MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
|
||||||
|
|
||||||
|
if (offer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Emulator.getPluginManager().fireEvent(new MarketPlaceItemCancelledEvent(offer)).isCancelled()) {
|
if (!Emulator.getPluginManager().fireEvent(new MarketPlaceItemCancelledEvent(offer)).isCancelled()) {
|
||||||
takeBackItem(habbo, offer);
|
takeBackItem(habbo, offer);
|
||||||
}
|
}
|
||||||
@@ -119,7 +125,7 @@ public class MarketPlace {
|
|||||||
|
|
||||||
public static List<MarketPlaceOffer> getOffers(int minPrice, int maxPrice, String search, int sort) {
|
public static List<MarketPlaceOffer> getOffers(int minPrice, int maxPrice, String search, int sort) {
|
||||||
List<MarketPlaceOffer> offers = new ArrayList<>(10);
|
List<MarketPlaceOffer> offers = new ArrayList<>(10);
|
||||||
String query = "SELECT B.* FROM marketplace_items a INNER JOIN (SELECT b.item_id AS base_item_id, b.limited_data AS ltd_data, marketplace_items.*, AVG(price) as avg, MIN(marketplace_items.price) as minPrice, MAX(marketplace_items.price) as maxPrice, COUNT(*) as number, (SELECT COUNT(*) FROM marketplace_items c INNER JOIN items as items_b ON c.item_id = items_b.id WHERE state = 2 AND items_b.item_id = base_item_id AND DATE(from_unixtime(sold_timestamp)) = CURDATE()) as sold_count_today FROM marketplace_items INNER JOIN items b ON marketplace_items.item_id = b.id INNER JOIN items_base bi ON b.item_id = bi.id INNER JOIN catalog_items ci ON bi.id = ci.item_ids WHERE price = (SELECT MIN(e.price) FROM marketplace_items e, items d WHERE e.item_id = d.id AND d.item_id = b.item_id AND e.state = 1 AND e.timestamp > ? GROUP BY d.item_id) AND state = 1 AND timestamp > ?";
|
String query = "SELECT B.* FROM marketplace_items a INNER JOIN (SELECT b.item_id AS base_item_id, b.limited_data AS ltd_data, marketplace_items.*, AVG(price) as avg, MIN(marketplace_items.price) as minPrice, MAX(marketplace_items.price) as maxPrice, COUNT(*) as number, (SELECT COUNT(*) FROM marketplace_items c INNER JOIN items as items_b ON c.item_id = items_b.id WHERE state = 2 AND items_b.item_id = base_item_id AND DATE(from_unixtime(sold_timestamp)) = CURDATE()) as sold_count_today FROM marketplace_items INNER JOIN items b ON marketplace_items.item_id = b.id INNER JOIN items_base bi ON b.item_id = bi.id INNER JOIN catalog_items ci ON bi.id = ci.item_ids WHERE price = (SELECT MIN(e.price) FROM marketplace_items e, items d WHERE e.item_id = d.id AND d.item_id = b.item_id AND e.state = 1 AND e.timestamp > ? AND e.price BETWEEN ? AND ? GROUP BY d.item_id) AND state = 1 AND timestamp > ? AND marketplace_items.price BETWEEN ? AND ?";
|
||||||
if (minPrice > 0) {
|
if (minPrice > 0) {
|
||||||
query += " AND CEIL(price + (price / 100)) >= ?";
|
query += " AND CEIL(price + (price / 100)) >= ?";
|
||||||
}
|
}
|
||||||
@@ -163,7 +169,11 @@ public class MarketPlace {
|
|||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) {
|
||||||
int paramIndex = 1;
|
int paramIndex = 1;
|
||||||
statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800);
|
statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800);
|
||||||
|
statement.setInt(paramIndex++, MINIMUM_LISTING_PRICE);
|
||||||
|
statement.setInt(paramIndex++, MAXIMUM_LISTING_PRICE);
|
||||||
statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800);
|
statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800);
|
||||||
|
statement.setInt(paramIndex++, MINIMUM_LISTING_PRICE);
|
||||||
|
statement.setInt(paramIndex++, MAXIMUM_LISTING_PRICE);
|
||||||
if (minPrice > 0) {
|
if (minPrice > 0) {
|
||||||
statement.setInt(paramIndex++, minPrice);
|
statement.setInt(paramIndex++, minPrice);
|
||||||
}
|
}
|
||||||
@@ -171,8 +181,9 @@ public class MarketPlace {
|
|||||||
statement.setInt(paramIndex++, maxPrice);
|
statement.setInt(paramIndex++, maxPrice);
|
||||||
}
|
}
|
||||||
if (!search.isEmpty()) {
|
if (!search.isEmpty()) {
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
String likeSearch = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(search) + "%";
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
statement.setString(paramIndex++, likeSearch);
|
||||||
|
statement.setString(paramIndex++, likeSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
@@ -264,7 +275,13 @@ public class MarketPlace {
|
|||||||
itemSet.first();
|
itemSet.first();
|
||||||
|
|
||||||
if (itemSet.getRow() > 0) {
|
if (itemSet.getRow() > 0) {
|
||||||
int price = MarketPlace.calculateCommision(set.getInt("price"));
|
int rawPrice = set.getInt("price");
|
||||||
|
if (!isValidListingPrice(rawPrice)) {
|
||||||
|
sendErrorMessage(client, set.getInt("item_id"), offerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int price = MarketPlace.calculateCommision(rawPrice);
|
||||||
if (set.getInt("state") != 1) {
|
if (set.getInt("state") != 1) {
|
||||||
sendErrorMessage(client, set.getInt("item_id"), offerId);
|
sendErrorMessage(client, set.getInt("item_id"), offerId);
|
||||||
} else if ((MARKETPLACE_CURRENCY == 0 && price > client.getHabbo().getHabboInfo().getCredits()) || (MARKETPLACE_CURRENCY > 0 && price > client.getHabbo().getHabboInfo().getCurrencyAmount(MARKETPLACE_CURRENCY))) {
|
} else if ((MARKETPLACE_CURRENCY == 0 && price > client.getHabbo().getHabboInfo().getCredits()) || (MARKETPLACE_CURRENCY > 0 && price > client.getHabbo().getHabboInfo().getCurrencyAmount(MARKETPLACE_CURRENCY))) {
|
||||||
@@ -278,8 +295,9 @@ public class MarketPlace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int soldTimestamp = Emulator.getIntUnixTimestamp();
|
||||||
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
|
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
|
||||||
updateOffer.setInt(1, Emulator.getIntUnixTimestamp());
|
updateOffer.setInt(1, soldTimestamp);
|
||||||
updateOffer.setInt(2, offerId);
|
updateOffer.setInt(2, offerId);
|
||||||
int updated = updateOffer.executeUpdate();
|
int updated = updateOffer.executeUpdate();
|
||||||
if (updated == 0) {
|
if (updated == 0) {
|
||||||
@@ -306,7 +324,11 @@ public class MarketPlace {
|
|||||||
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
|
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
|
||||||
|
|
||||||
if (habbo != null) {
|
if (habbo != null) {
|
||||||
habbo.getInventory().getOffer(offerId).setState(MarketPlaceState.SOLD);
|
MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
|
||||||
|
if (offer != null) {
|
||||||
|
offer.setState(MarketPlaceState.SOLD);
|
||||||
|
offer.setSoldTimestamp(soldTimestamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,7 +374,7 @@ public class MarketPlace {
|
|||||||
if (item == null || client == null)
|
if (item == null || client == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!item.getBaseItem().allowMarketplace() || price < 0)
|
if (!item.getBaseItem().allowMarketplace() || !isValidListingPrice(price))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
MarketPlaceItemOfferedEvent event = new MarketPlaceItemOfferedEvent(client.getHabbo(), item, price);
|
MarketPlaceItemOfferedEvent event = new MarketPlaceItemOfferedEvent(client.getHabbo(), item, price);
|
||||||
@@ -368,6 +390,11 @@ public class MarketPlace {
|
|||||||
event.item.setFromGift(false);
|
event.item.setFromGift(false);
|
||||||
|
|
||||||
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
|
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
|
||||||
|
if (!offer.isPersisted()) {
|
||||||
|
LOGGER.warn("Marketplace offer insert failed for user {} item {}", client.getHabbo().getHabboInfo().getId(), event.item.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
client.getHabbo().getInventory().addMarketplaceOffer(offer);
|
client.getHabbo().getInventory().addMarketplaceOffer(offer);
|
||||||
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
|
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
|
||||||
item.setUserId(-1);
|
item.setUserId(-1);
|
||||||
@@ -387,11 +414,12 @@ public class MarketPlace {
|
|||||||
synchronized (client.getHabbo().getInventory()) {
|
synchronized (client.getHabbo().getInventory()) {
|
||||||
for (MarketPlaceOffer offer : offers) {
|
for (MarketPlaceOffer offer : offers) {
|
||||||
if (offer.getState().equals(MarketPlaceState.SOLD)) {
|
if (offer.getState().equals(MarketPlaceState.SOLD)) {
|
||||||
client.getHabbo().getInventory().removeMarketplaceOffer(offer);
|
if (removeUser(offer)) {
|
||||||
credits += offer.getPrice();
|
client.getHabbo().getInventory().removeMarketplaceOffer(offer);
|
||||||
removeUser(offer);
|
credits += offer.getPrice();
|
||||||
offer.needsUpdate(true);
|
offer.needsUpdate(true);
|
||||||
Emulator.getThreading().run(offer);
|
Emulator.getThreading().run(offer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,18 +433,24 @@ public class MarketPlace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void removeUser(MarketPlaceOffer offer) {
|
private static boolean removeUser(MarketPlaceOffer offer) {
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE marketplace_items SET user_id = ? WHERE id = ?")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE marketplace_items SET user_id = ? WHERE id = ?")) {
|
||||||
statement.setInt(1, -1);
|
statement.setInt(1, -1);
|
||||||
statement.setInt(2, offer.getOfferId());
|
statement.setInt(2, offer.getOfferId());
|
||||||
statement.execute();
|
return statement.executeUpdate() > 0;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static int calculateCommision(int price) {
|
public static int calculateCommision(int price) {
|
||||||
return price + (int) Math.ceil(price / 100.0);
|
long commission = price + (long) Math.ceil(price / 100.0);
|
||||||
|
return commission > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) commission;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isValidListingPrice(int price) {
|
||||||
|
return price >= MINIMUM_LISTING_PRICE && price <= MAXIMUM_LISTING_PRICE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -98,6 +98,10 @@ public class MarketPlaceOffer implements Runnable {
|
|||||||
return this.offerId;
|
return this.offerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPersisted() {
|
||||||
|
return this.offerId > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public void setOfferId(int offerId) {
|
public void setOfferId(int offerId) {
|
||||||
this.offerId = offerId;
|
this.offerId = offerId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ public class AlertCommand extends Command {
|
|||||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUsername);
|
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUsername);
|
||||||
|
|
||||||
if (habbo != null) {
|
if (habbo != null) {
|
||||||
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
habbo.alert(message + "\r\n -" + gameClient.getHabbo().getHabboInfo().getUsername());
|
habbo.alert(message + "\r\n -" + gameClient.getHabbo().getHabboInfo().getUsername());
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_alert.message_send").replace("%user%", targetUsername), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_alert.message_send").replace("%user%", targetUsername), RoomChatMessageBubbles.ALERT);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class BanCommand extends Command {
|
public class BanCommand extends Command {
|
||||||
public BanCommand() {
|
public BanCommand() {
|
||||||
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
|
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
|
||||||
@@ -58,7 +60,7 @@ public class BanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) {
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -72,7 +74,13 @@ public class BanCommand extends Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1).get(0);
|
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1);
|
||||||
|
if (bans == null || bans.isEmpty()) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.user_offline"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModToolBan ban = bans.get(0);
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.eu.habbo.habbohotel.commands;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
|
|
||||||
|
final class CommandTargetGuard {
|
||||||
|
private CommandTargetGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean canTarget(Habbo moderator, Habbo target) {
|
||||||
|
return target != null && canTarget(moderator, target.getHabboInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean canTarget(Habbo moderator, HabboInfo target) {
|
||||||
|
if (moderator == null || target == null || moderator.getHabboInfo().getId() == target.getId()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int moderatorRankId = moderator.getHabboInfo().getRank().getId();
|
||||||
|
int targetRankId = target.getRank().getId();
|
||||||
|
|
||||||
|
return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean canAssignRank(Habbo moderator, Rank rank) {
|
||||||
|
if (moderator == null || rank == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int moderatorRankId = moderator.getHabboInfo().getRank().getId();
|
||||||
|
int targetRankId = rank.getId();
|
||||||
|
|
||||||
|
return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCoreRank(int rankId) {
|
||||||
|
int highestRankId = Emulator.getGameEnvironment().getPermissionsManager().getAllRanks().stream()
|
||||||
|
.mapToInt(Rank::getId)
|
||||||
|
.max()
|
||||||
|
.orElse(0);
|
||||||
|
|
||||||
|
return highestRankId > 0 && rankId >= highestRankId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ public class CommandsCommand extends Command {
|
|||||||
|
|
||||||
for (Command c : commands) {
|
for (Command c : commands) {
|
||||||
String textKey = "commands.description." + c.permission;
|
String textKey = "commands.description." + c.permission;
|
||||||
String commandText = Emulator.getTexts().getValue(textKey, "");
|
String commandText = Emulator.getTexts().getValueQuietly(textKey, "");
|
||||||
String commandLine = ":" + c.keys[0];
|
String commandLine = ":" + c.keys[0];
|
||||||
String description = "";
|
String description = "";
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class DisconnectCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target.getHabboInfo().getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) {
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_disconnect.higher_rank"), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_disconnect.higher_rank"), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ public class GivePrefixCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect);
|
UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect);
|
||||||
prefix.run();
|
prefix.run();
|
||||||
target.getInventory().getPrefixesComponent().addPrefix(prefix);
|
target.getInventory().getPrefixesComponent().addPrefix(prefix);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class GiveRankCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rank != null) {
|
if (rank != null) {
|
||||||
if (rank.getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) {
|
if (!CommandTargetGuard.canAssignRank(gameClient.getHabbo(), rank)) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ public class GiveRankCommand extends Command {
|
|||||||
HabboInfo habbo = HabboManager.getOfflineHabboInfo(params[1]);
|
HabboInfo habbo = HabboManager.getOfflineHabboInfo(params[1]);
|
||||||
|
|
||||||
if (habbo != null) {
|
if (habbo != null) {
|
||||||
if (habbo.getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) {
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher.other").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher.other").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class IPBanCommand extends Command {
|
public class IPBanCommand extends Command {
|
||||||
public final static int TEN_YEARS = 315569260;
|
public final static int TEN_YEARS = 315569260;
|
||||||
|
|
||||||
@@ -45,17 +47,17 @@ public class IPBanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) {
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||||
count++;
|
count += bans != null ? bans.size() : 0;
|
||||||
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
|
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
|
||||||
if (h != null) {
|
if (h != null) {
|
||||||
count++;
|
bans = Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||||
Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
count += bans != null ? bans.size() : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MachineBanCommand extends Command {
|
public class MachineBanCommand extends Command {
|
||||||
public MachineBanCommand() {
|
public MachineBanCommand() {
|
||||||
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
|
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
|
||||||
@@ -41,12 +43,13 @@ public class MachineBanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) {
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
count = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1).size();
|
List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1);
|
||||||
|
count = bans != null ? bans.size() : 0;
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ public class MuteCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int duration = Integer.MAX_VALUE;
|
int duration = Integer.MAX_VALUE;
|
||||||
|
|
||||||
if (params.length == 3) {
|
if (params.length == 3) {
|
||||||
|
|||||||
@@ -32,35 +32,44 @@ public class RedeemCommand extends Command {
|
|||||||
for (HabboItem item : gameClient.getHabbo().getInventory().getItemsComponent().getItemsAsValueCollection()) {
|
for (HabboItem item : gameClient.getHabbo().getInventory().getItemsComponent().getItemsAsValueCollection()) {
|
||||||
if (item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_") || item.getBaseItem().getName().startsWith("DF_") || item.getBaseItem().getName().startsWith("PF_")) {
|
if (item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_") || item.getBaseItem().getName().startsWith("DF_") || item.getBaseItem().getName().startsWith("PF_")) {
|
||||||
if (item.getUserId() == gameClient.getHabbo().getHabboInfo().getId()) {
|
if (item.getUserId() == gameClient.getHabbo().getHabboInfo().getId()) {
|
||||||
items.add(item);
|
boolean redeemable = false;
|
||||||
if ((item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_")) && !item.getBaseItem().getName().contains("_diamond_")) {
|
if ((item.getBaseItem().getName().startsWith("CF_") || item.getBaseItem().getName().startsWith("CFC_")) && !item.getBaseItem().getName().contains("_diamond_")) {
|
||||||
try {
|
Integer amount = parsePositiveRedeemValue(item.getBaseItem().getName(), 1);
|
||||||
credits += Integer.parseInt(item.getBaseItem().getName().split("_")[1]);
|
if (amount != null) {
|
||||||
} catch (Exception e) {
|
Integer total = addRedeemValue(credits, amount);
|
||||||
|
if (total != null) {
|
||||||
|
credits = total;
|
||||||
|
redeemable = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (item.getBaseItem().getName().startsWith("PF_")) {
|
} else if (item.getBaseItem().getName().startsWith("PF_")) {
|
||||||
try {
|
Integer amount = parsePositiveRedeemValue(item.getBaseItem().getName(), 1);
|
||||||
pixels += Integer.parseInt(item.getBaseItem().getName().split("_")[1]);
|
if (amount != null) {
|
||||||
} catch (Exception e) {
|
Integer total = addRedeemValue(pixels, amount);
|
||||||
|
if (total != null) {
|
||||||
|
pixels = total;
|
||||||
|
redeemable = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (item.getBaseItem().getName().startsWith("DF_")) {
|
} else if (item.getBaseItem().getName().startsWith("DF_")) {
|
||||||
int pointsType;
|
Integer pointsType = parsePositiveRedeemValue(item.getBaseItem().getName(), 1);
|
||||||
int pointsAmount;
|
Integer pointsAmount = parsePositiveRedeemValue(item.getBaseItem().getName(), 2);
|
||||||
|
|
||||||
pointsType = Integer.parseInt(item.getBaseItem().getName().split("_")[1]);
|
if (pointsType != null && pointsAmount != null && addRedeemPoints(points, pointsType, pointsAmount)) {
|
||||||
pointsAmount = Integer.parseInt(item.getBaseItem().getName().split("_")[2]);
|
redeemable = true;
|
||||||
|
}
|
||||||
points.adjustOrPutValue(pointsType, pointsAmount, pointsAmount);
|
|
||||||
}
|
}
|
||||||
else if (item.getBaseItem().getName().startsWith("CF_diamond_")) {
|
else if (item.getBaseItem().getName().startsWith("CF_diamond_")) {
|
||||||
int pointsType;
|
Integer pointsAmount = parsePositiveRedeemValue(item.getBaseItem().getName(), 2);
|
||||||
int pointsAmount;
|
|
||||||
|
|
||||||
pointsType = 5;
|
if (pointsAmount != null && addRedeemPoints(points, 5, pointsAmount)) {
|
||||||
pointsAmount = Integer.parseInt(item.getBaseItem().getName().split("_")[2]);
|
redeemable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
points.adjustOrPutValue(pointsType, pointsAmount, pointsAmount);
|
if (redeemable) {
|
||||||
|
items.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,4 +112,41 @@ public class RedeemCommand extends Command {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Integer parsePositiveRedeemValue(String itemName, int index) {
|
||||||
|
if (itemName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = itemName.split("_");
|
||||||
|
if (index < 0 || index >= parts.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int value = Integer.parseInt(parts[index]);
|
||||||
|
return value > 0 ? value : null;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Integer addRedeemValue(int current, int amount) {
|
||||||
|
try {
|
||||||
|
return Math.addExact(current, amount);
|
||||||
|
} catch (ArithmeticException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean addRedeemPoints(TIntIntMap points, int pointsType, int amount) {
|
||||||
|
int current = points.get(pointsType);
|
||||||
|
Integer total = addRedeemValue(current, amount);
|
||||||
|
if (total == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
points.put(pointsType, total);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ public class RemovePrefixCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (prefixIdStr.equalsIgnoreCase("all")) {
|
if (prefixIdStr.equalsIgnoreCase("all")) {
|
||||||
List<UserPrefix> prefixes = target.getInventory().getPrefixesComponent().getPrefixes();
|
List<UserPrefix> prefixes = target.getInventory().getPrefixesComponent().getPrefixes();
|
||||||
for (UserPrefix prefix : prefixes) {
|
for (UserPrefix prefix : prefixes) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class SuperbanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) {
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ public class UnmuteCommand extends Command {
|
|||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_unmute.not_found").replace("%user%", params[1]), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_unmute.not_found").replace("%user%", params[1]), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!habbo.getHabboStats().allowTalk() || (habbo.getHabboInfo().getCurrentRoom() != null && habbo.getHabboInfo().getCurrentRoom().isMuted(habbo))) {
|
if (!habbo.getHabboStats().allowTalk() || (habbo.getHabboInfo().getCurrentRoom() != null && habbo.getHabboInfo().getCurrentRoom().isMuted(habbo))) {
|
||||||
if (!habbo.getHabboStats().allowTalk()) {
|
if (!habbo.getHabboStats().allowTalk()) {
|
||||||
habbo.unMute();
|
habbo.unMute();
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.eu.habbo.habbohotel.earnings;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public enum EarningsCategory {
|
||||||
|
DAILY_GIFT("daily_gift"),
|
||||||
|
GAMES("games"),
|
||||||
|
ACHIEVEMENTS("achievements"),
|
||||||
|
MARKETPLACE("marketplace"),
|
||||||
|
HC_PAYDAY("hc_payday"),
|
||||||
|
LEVEL_PROGRESS("level_progress"),
|
||||||
|
DONATIONS("donations"),
|
||||||
|
BONUS_BAG("bonus_bag"),
|
||||||
|
MYSTERY_BOXES("mystery_boxes"),
|
||||||
|
CLUB_JOB("club_job");
|
||||||
|
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
EarningsCategory(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<EarningsCategory> fromKey(String key) {
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = key.trim().toLowerCase();
|
||||||
|
return Arrays.stream(values())
|
||||||
|
.filter(category -> category.key.equals(normalized))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
package com.eu.habbo.habbohotel.earnings;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||||
|
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionHabboClub;
|
||||||
|
import com.eu.habbo.messages.outgoing.users.AddUserBadgeComposer;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.SQLIntegrityConstraintViolationException;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class EarningsCenterManager {
|
||||||
|
public static final String CONFIG_PREFIX = "earnings.";
|
||||||
|
private static final int DEFAULT_COOLDOWN_SECONDS = 86400;
|
||||||
|
private static final int DEFAULT_POINTS_TYPE = 5;
|
||||||
|
private static final int MAX_CONFIGURED_REWARD = 1_000_000;
|
||||||
|
private static final int MAX_ITEM_QUANTITY = 100;
|
||||||
|
private static final int MAX_HC_DAYS = 365;
|
||||||
|
|
||||||
|
private final ConfigSource config;
|
||||||
|
private final ClaimRepository claims;
|
||||||
|
private final RewardApplier rewards;
|
||||||
|
private final NativeIntegration nativeIntegration;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
public EarningsCenterManager() {
|
||||||
|
this(new EmulatorConfigSource(), new JdbcClaimRepository(), new HabboRewardApplier(), new DefaultNativeIntegration(), Clock.systemUTC());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsCenterManager(ConfigSource config, ClaimRepository claims, RewardApplier rewards, Clock clock) {
|
||||||
|
this(config, claims, rewards, new NoopNativeIntegration(), clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsCenterManager(ConfigSource config, ClaimRepository claims, RewardApplier rewards, NativeIntegration nativeIntegration, Clock clock) {
|
||||||
|
this.config = config;
|
||||||
|
this.claims = claims;
|
||||||
|
this.rewards = rewards;
|
||||||
|
this.nativeIntegration = nativeIntegration;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EarningsEntry> getEntries(Habbo habbo) {
|
||||||
|
int userId = getUserId(habbo);
|
||||||
|
int now = now();
|
||||||
|
List<EarningsEntry> entries = new ArrayList<>();
|
||||||
|
|
||||||
|
for (EarningsCategory category : EarningsCategory.values()) {
|
||||||
|
entries.add(buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsClaimResult claim(Habbo habbo, String categoryKey) {
|
||||||
|
Optional<EarningsCategory> requestedCategory = EarningsCategory.fromKey(categoryKey);
|
||||||
|
if (requestedCategory.isEmpty()) {
|
||||||
|
return new EarningsClaimResult(null, EarningsClaimResult.Status.UNKNOWN_CATEGORY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim(habbo, requestedCategory.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EarningsClaimResult> claimAll(Habbo habbo) {
|
||||||
|
List<EarningsClaimResult> results = new ArrayList<>();
|
||||||
|
|
||||||
|
for (EarningsCategory category : EarningsCategory.values()) {
|
||||||
|
results.add(claim(habbo, category));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EarningsClaimResult claim(Habbo habbo, EarningsCategory category) {
|
||||||
|
int userId = getUserId(habbo);
|
||||||
|
int now = now();
|
||||||
|
CategoryDefinition definition = loadDefinition(habbo, category);
|
||||||
|
|
||||||
|
if (!definition.enabled()) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.DISABLED, buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.nativeIntegration.handles(category) && nativeEnabled(category)) {
|
||||||
|
return claimNative(habbo, userId, category, now, definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.rewards().isEmpty()) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEligibleForProgressClaim(habbo, category)) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
String periodKey = periodKey(habbo, category, now, definition.cooldownSeconds());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.claims.recordClaim(userId, category.getKey(), periodKey, now)) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.ALREADY_CLAIMED, buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rewards.grant(habbo, definition.rewards());
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(habbo, userId, category, now));
|
||||||
|
} catch (SQLException e) {
|
||||||
|
try {
|
||||||
|
this.claims.removeClaim(userId, category.getKey(), periodKey);
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
}
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EarningsClaimResult claimNative(Habbo habbo, int userId, EarningsCategory category, int now, CategoryDefinition definition) {
|
||||||
|
try {
|
||||||
|
if (definition.rewards().isEmpty() || !this.nativeIntegration.hasClaim(habbo, category)) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.nativeIntegration.claim(habbo, category)
|
||||||
|
? new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(habbo, userId, category, now))
|
||||||
|
: new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EarningsEntry buildEntry(Habbo habbo, int userId, EarningsCategory category, int now) {
|
||||||
|
CategoryDefinition definition = loadDefinition(habbo, category);
|
||||||
|
boolean claimable = false;
|
||||||
|
int nextClaimAt = 0;
|
||||||
|
|
||||||
|
if (definition.enabled() && !definition.rewards().isEmpty()) {
|
||||||
|
if (this.nativeIntegration.handles(category) && nativeEnabled(category)) {
|
||||||
|
try {
|
||||||
|
claimable = this.nativeIntegration.hasClaim(habbo, category);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
claimable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EarningsEntry(category, true, claimable, 0, definition.rewards());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEligibleForProgressClaim(habbo, category)) {
|
||||||
|
return new EarningsEntry(category, true, false, 0, definition.rewards());
|
||||||
|
}
|
||||||
|
|
||||||
|
String periodKey = periodKey(habbo, category, now, definition.cooldownSeconds());
|
||||||
|
|
||||||
|
try {
|
||||||
|
claimable = !this.claims.hasClaim(userId, category.getKey(), periodKey);
|
||||||
|
nextClaimAt = claimable ? 0 : nextPeriodStart(now, definition.cooldownSeconds());
|
||||||
|
} catch (SQLException e) {
|
||||||
|
claimable = false;
|
||||||
|
nextClaimAt = nextPeriodStart(now, definition.cooldownSeconds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EarningsEntry(category, definition.enabled(), claimable, nextClaimAt, definition.rewards());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CategoryDefinition loadDefinition(Habbo habbo, EarningsCategory category) {
|
||||||
|
String key = CONFIG_PREFIX + category.getKey() + ".";
|
||||||
|
boolean enabled = this.config.getBoolean(CONFIG_PREFIX + "enabled", false)
|
||||||
|
&& this.config.getBoolean(key + "enabled", true);
|
||||||
|
int cooldown = Math.max(60, this.config.getInt(key + "cooldown.seconds", DEFAULT_COOLDOWN_SECONDS));
|
||||||
|
int pointsType = Math.max(0, this.config.getInt(key + "points.type", DEFAULT_POINTS_TYPE));
|
||||||
|
List<EarningsReward> rewards = new ArrayList<>();
|
||||||
|
|
||||||
|
if (nativeEnabled(category) && this.nativeIntegration.handles(category)) {
|
||||||
|
try {
|
||||||
|
rewards.addAll(this.nativeIntegration.rewards(habbo, category));
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addReward(rewards, EarningsReward.TYPE_CREDITS, this.config.getInt(key + "credits", 0), 0);
|
||||||
|
addReward(rewards, EarningsReward.TYPE_PIXELS, this.config.getInt(key + "pixels", 0), 0);
|
||||||
|
addReward(rewards, EarningsReward.TYPE_POINTS, this.config.getInt(key + "points", 0), pointsType);
|
||||||
|
addBadgeReward(rewards, this.config.getValue(key + "badge", ""));
|
||||||
|
addItemReward(rewards, this.config.getInt(key + "item_id", 0), this.config.getInt(key + "item.quantity", 1));
|
||||||
|
addHcReward(rewards, this.config.getInt(key + "hc.days", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CategoryDefinition(enabled, cooldown, rewards);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean nativeEnabled(EarningsCategory category) {
|
||||||
|
return this.config.getBoolean(CONFIG_PREFIX + category.getKey() + ".native.enabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEligibleForProgressClaim(Habbo habbo, EarningsCategory category) {
|
||||||
|
if (!nativeEnabled(category) || habbo == null || habbo.getHabboStats() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category == EarningsCategory.ACHIEVEMENTS) {
|
||||||
|
int minimumScore = Math.max(1, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".min_score", 1));
|
||||||
|
return habbo.getHabboStats().getAchievementScore() >= minimumScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category == EarningsCategory.LEVEL_PROGRESS) {
|
||||||
|
int minimumLevel = Math.max(0, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".min_level", 1));
|
||||||
|
int highestLevel = Math.max(habbo.getHabboStats().citizenshipLevel, habbo.getHabboStats().helpersLevel);
|
||||||
|
return highestLevel >= minimumLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addReward(List<EarningsReward> rewards, String type, int amount, int pointsType) {
|
||||||
|
int clampedAmount = Math.min(Math.max(0, amount), MAX_CONFIGURED_REWARD);
|
||||||
|
if (clampedAmount > 0) {
|
||||||
|
rewards.add(new EarningsReward(type, clampedAmount, pointsType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBadgeReward(List<EarningsReward> rewards, String badgeCode) {
|
||||||
|
if (badgeCode == null || !badgeCode.matches("[A-Za-z0-9_\\-]{1,64}")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards.add(new EarningsReward(EarningsReward.TYPE_BADGE, 1, 0, badgeCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addItemReward(List<EarningsReward> rewards, int itemId, int quantity) {
|
||||||
|
if (itemId <= 0 || quantity <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards.add(new EarningsReward(EarningsReward.TYPE_ITEM, Math.min(quantity, MAX_ITEM_QUANTITY), 0, String.valueOf(itemId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addHcReward(List<EarningsReward> rewards, int days) {
|
||||||
|
if (days <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards.add(new EarningsReward(EarningsReward.TYPE_HC_DAYS, Math.min(days, MAX_HC_DAYS), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getUserId(Habbo habbo) {
|
||||||
|
if (habbo == null || habbo.getHabboInfo() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return habbo.getHabboInfo().getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int now() {
|
||||||
|
return (int) (this.clock.instant().getEpochSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String periodKey(Habbo habbo, EarningsCategory category, int now, int cooldownSeconds) {
|
||||||
|
if (nativeEnabled(category) && habbo != null && habbo.getHabboStats() != null) {
|
||||||
|
if (category == EarningsCategory.ACHIEVEMENTS) {
|
||||||
|
int scoreStep = Math.max(1, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".score.step", 100));
|
||||||
|
return "score:" + (habbo.getHabboStats().getAchievementScore() / scoreStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category == EarningsCategory.LEVEL_PROGRESS) {
|
||||||
|
int highestLevel = Math.max(habbo.getHabboStats().citizenshipLevel, habbo.getHabboStats().helpersLevel);
|
||||||
|
return "level:" + highestLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.valueOf(now / cooldownSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nextPeriodStart(int now, int cooldownSeconds) {
|
||||||
|
return ((now / cooldownSeconds) + 1) * cooldownSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CategoryDefinition(boolean enabled, int cooldownSeconds, List<EarningsReward> rewards) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ConfigSource {
|
||||||
|
boolean getBoolean(String key, boolean defaultValue);
|
||||||
|
|
||||||
|
int getInt(String key, int defaultValue);
|
||||||
|
|
||||||
|
String getValue(String key, String defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ClaimRepository {
|
||||||
|
boolean hasClaim(int userId, String category, String periodKey) throws SQLException;
|
||||||
|
|
||||||
|
boolean recordClaim(int userId, String category, String periodKey, int claimedAt) throws SQLException;
|
||||||
|
|
||||||
|
void removeClaim(int userId, String category, String periodKey) throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface RewardApplier {
|
||||||
|
void grant(Habbo habbo, List<EarningsReward> rewards) throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface NativeIntegration {
|
||||||
|
boolean handles(EarningsCategory category);
|
||||||
|
|
||||||
|
boolean hasClaim(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||||
|
|
||||||
|
List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||||
|
|
||||||
|
boolean claim(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class EmulatorConfigSource implements ConfigSource {
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defaultValue) {
|
||||||
|
return Emulator.getConfig().getBoolean(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInt(String key, int defaultValue) {
|
||||||
|
return Emulator.getConfig().getInt(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue(String key, String defaultValue) {
|
||||||
|
return Emulator.getConfig().getValue(key, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class JdbcClaimRepository implements ClaimRepository {
|
||||||
|
@Override
|
||||||
|
public boolean hasClaim(int userId, String category, String periodKey) throws SQLException {
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM users_earnings_claims WHERE user_id = ? AND category = ? AND period_key = ? LIMIT 1")) {
|
||||||
|
statement.setInt(1, userId);
|
||||||
|
statement.setString(2, category);
|
||||||
|
statement.setString(3, periodKey);
|
||||||
|
return statement.executeQuery().next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean recordClaim(int userId, String category, String periodKey, int claimedAt) throws SQLException {
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("INSERT INTO users_earnings_claims (user_id, category, period_key, claimed_at) VALUES (?, ?, ?, FROM_UNIXTIME(?))")) {
|
||||||
|
statement.setInt(1, userId);
|
||||||
|
statement.setString(2, category);
|
||||||
|
statement.setString(3, periodKey);
|
||||||
|
statement.setInt(4, claimedAt);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
} catch (SQLIntegrityConstraintViolationException duplicate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeClaim(int userId, String category, String periodKey) throws SQLException {
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("DELETE FROM users_earnings_claims WHERE user_id = ? AND category = ? AND period_key = ? LIMIT 1")) {
|
||||||
|
statement.setInt(1, userId);
|
||||||
|
statement.setString(2, category);
|
||||||
|
statement.setString(3, periodKey);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HabboRewardApplier implements RewardApplier {
|
||||||
|
@Override
|
||||||
|
public void grant(Habbo habbo, List<EarningsReward> rewards) throws SQLException {
|
||||||
|
if (habbo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (EarningsReward reward : rewards) {
|
||||||
|
switch (reward.getType()) {
|
||||||
|
case EarningsReward.TYPE_CREDITS -> habbo.giveCredits(reward.getAmount());
|
||||||
|
case EarningsReward.TYPE_PIXELS -> habbo.givePixels(reward.getAmount());
|
||||||
|
case EarningsReward.TYPE_POINTS -> habbo.givePoints(reward.getPointsType(), reward.getAmount());
|
||||||
|
case EarningsReward.TYPE_BADGE -> grantBadge(habbo, reward.getData());
|
||||||
|
case EarningsReward.TYPE_ITEM -> grantItem(habbo, Integer.parseInt(reward.getData()), reward.getAmount());
|
||||||
|
case EarningsReward.TYPE_HC_DAYS -> grantHcDays(habbo, reward.getAmount());
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void grantBadge(Habbo habbo, String badgeCode) throws SQLException {
|
||||||
|
if (habbo.getInventory().getBadgesComponent().hasBadge(badgeCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HabboBadge badge = new HabboBadge(0, badgeCode, 0, habbo);
|
||||||
|
badge.run();
|
||||||
|
habbo.getInventory().getBadgesComponent().addBadge(badge);
|
||||||
|
if (habbo.getClient() != null) {
|
||||||
|
habbo.getClient().sendResponse(new AddUserBadgeComposer(badge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void grantItem(Habbo habbo, int itemId, int quantity) throws SQLException {
|
||||||
|
if (!itemExists(itemId)) {
|
||||||
|
throw new SQLException("Unknown earnings item reward " + itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data) VALUES (?, ?, '')")) {
|
||||||
|
for (int i = 0; i < quantity; i++) {
|
||||||
|
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||||
|
statement.setInt(2, itemId);
|
||||||
|
statement.addBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
statement.executeBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean itemExists(int itemId) throws SQLException {
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("SELECT id FROM items_base WHERE id = ? LIMIT 1")) {
|
||||||
|
statement.setInt(1, itemId);
|
||||||
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
|
return set.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void grantHcDays(Habbo habbo, int days) throws SQLException {
|
||||||
|
int now = Emulator.getIntUnixTimestamp();
|
||||||
|
int current = habbo.getHabboStats().getClubExpireTimestamp();
|
||||||
|
int newExpire = (current > now ? current : now) + (days * 86400);
|
||||||
|
|
||||||
|
habbo.getHabboStats().setClubExpireTimestamp(newExpire);
|
||||||
|
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET club_expire_timestamp = ? WHERE user_id = ? LIMIT 1")) {
|
||||||
|
statement.setInt(1, newExpire);
|
||||||
|
statement.setInt(2, habbo.getHabboInfo().getId());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoopNativeIntegration implements NativeIntegration {
|
||||||
|
@Override
|
||||||
|
public boolean handles(EarningsCategory category) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasClaim(Habbo habbo, EarningsCategory category) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean claim(Habbo habbo, EarningsCategory category) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DefaultNativeIntegration implements NativeIntegration {
|
||||||
|
@Override
|
||||||
|
public boolean handles(EarningsCategory category) {
|
||||||
|
return category == EarningsCategory.MARKETPLACE || category == EarningsCategory.HC_PAYDAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasClaim(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||||
|
return !rewards(habbo, category).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||||
|
if (habbo == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category == EarningsCategory.MARKETPLACE) {
|
||||||
|
int soldPriceTotal = habbo.getInventory().getSoldPriceTotal();
|
||||||
|
if (soldPriceTotal <= 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MarketPlace.MARKETPLACE_CURRENCY == 0) {
|
||||||
|
return List.of(new EarningsReward(EarningsReward.TYPE_CREDITS, soldPriceTotal, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(new EarningsReward(EarningsReward.TYPE_POINTS, soldPriceTotal, MarketPlace.MARKETPLACE_CURRENCY));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category == EarningsCategory.HC_PAYDAY) {
|
||||||
|
return hcPaydayRewards(habbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean claim(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||||
|
if (habbo == null || habbo.getClient() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category == EarningsCategory.MARKETPLACE) {
|
||||||
|
if (habbo.getInventory().getSoldPriceTotal() <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketPlace.getCredits(habbo.getClient());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category == EarningsCategory.HC_PAYDAY) {
|
||||||
|
if (hcPaydayRewards(habbo).isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionHabboClub.processUnclaimed(habbo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EarningsReward> hcPaydayRewards(Habbo habbo) throws SQLException {
|
||||||
|
List<EarningsReward> rewards = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("SELECT currency, SUM(total_payout) AS amount FROM logs_hc_payday WHERE user_id = ? AND claimed = 0 GROUP BY currency")) {
|
||||||
|
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||||
|
|
||||||
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
|
while (set.next()) {
|
||||||
|
EarningsReward reward = currencyReward(set.getString("currency"), set.getInt("amount"));
|
||||||
|
if (reward != null) {
|
||||||
|
rewards.add(reward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewards;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EarningsReward currencyReward(String currency, int amount) {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = currency == null ? "" : currency.trim().toLowerCase();
|
||||||
|
return switch (normalized) {
|
||||||
|
case "credits", "credit", "coins", "coin" -> new EarningsReward(EarningsReward.TYPE_CREDITS, amount, 0);
|
||||||
|
case "duckets", "ducket", "pixels", "pixel" -> new EarningsReward(EarningsReward.TYPE_PIXELS, amount, 0);
|
||||||
|
case "diamonds", "diamond" -> new EarningsReward(EarningsReward.TYPE_POINTS, amount, 5);
|
||||||
|
default -> {
|
||||||
|
try {
|
||||||
|
yield new EarningsReward(EarningsReward.TYPE_POINTS, amount, Math.max(0, Integer.parseInt(normalized)));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
yield null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.eu.habbo.habbohotel.earnings;
|
||||||
|
|
||||||
|
public class EarningsClaimResult {
|
||||||
|
public enum Status {
|
||||||
|
SUCCESS,
|
||||||
|
DISABLED,
|
||||||
|
UNKNOWN_CATEGORY,
|
||||||
|
ALREADY_CLAIMED,
|
||||||
|
NO_REWARD,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
private final EarningsCategory category;
|
||||||
|
private final Status status;
|
||||||
|
private final EarningsEntry entry;
|
||||||
|
|
||||||
|
public EarningsClaimResult(EarningsCategory category, Status status, EarningsEntry entry) {
|
||||||
|
this.category = category;
|
||||||
|
this.status = status;
|
||||||
|
this.entry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsCategory getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCategoryKey() {
|
||||||
|
return category == null ? "" : category.getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return status == Status.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsEntry getEntry() {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.eu.habbo.habbohotel.earnings;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class EarningsEntry {
|
||||||
|
private final EarningsCategory category;
|
||||||
|
private final boolean enabled;
|
||||||
|
private final boolean claimable;
|
||||||
|
private final int nextClaimAt;
|
||||||
|
private final List<EarningsReward> rewards;
|
||||||
|
|
||||||
|
public EarningsEntry(EarningsCategory category, boolean enabled, boolean claimable, int nextClaimAt, List<EarningsReward> rewards) {
|
||||||
|
this.category = category;
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.claimable = claimable;
|
||||||
|
this.nextClaimAt = Math.max(0, nextClaimAt);
|
||||||
|
this.rewards = List.copyOf(rewards);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsCategory getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClaimable() {
|
||||||
|
return claimable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNextClaimAt() {
|
||||||
|
return nextClaimAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EarningsReward> getRewards() {
|
||||||
|
return rewards;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.eu.habbo.habbohotel.earnings;
|
||||||
|
|
||||||
|
public class EarningsReward {
|
||||||
|
public static final String TYPE_CREDITS = "credits";
|
||||||
|
public static final String TYPE_PIXELS = "pixels";
|
||||||
|
public static final String TYPE_POINTS = "points";
|
||||||
|
public static final String TYPE_BADGE = "badge";
|
||||||
|
public static final String TYPE_ITEM = "item";
|
||||||
|
public static final String TYPE_HC_DAYS = "hc_days";
|
||||||
|
|
||||||
|
private final String type;
|
||||||
|
private final int amount;
|
||||||
|
private final int pointsType;
|
||||||
|
private final String data;
|
||||||
|
|
||||||
|
public EarningsReward(String type, int amount, int pointsType) {
|
||||||
|
this(type, amount, pointsType, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsReward(String type, int amount, int pointsType, String data) {
|
||||||
|
this.type = type;
|
||||||
|
this.amount = Math.max(0, amount);
|
||||||
|
this.pointsType = Math.max(0, pointsType);
|
||||||
|
this.data = data == null ? "" : data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPointsType() {
|
||||||
|
return pointsType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class GameClient {
|
public class GameClient {
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ public class GameClient {
|
|||||||
private final LatencyTracker latencyTracker;
|
private final LatencyTracker latencyTracker;
|
||||||
|
|
||||||
private Habbo habbo;
|
private Habbo habbo;
|
||||||
|
private final AtomicBoolean disposed = new AtomicBoolean(false);
|
||||||
private boolean handshakeFinished;
|
private boolean handshakeFinished;
|
||||||
private String machineId = "";
|
private String machineId = "";
|
||||||
private String ssoTicket = "";
|
private String ssoTicket = "";
|
||||||
@@ -149,6 +151,14 @@ public class GameClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
|
this.dispose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose(boolean allowSessionResume) {
|
||||||
|
if (!this.disposed.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.channel.close();
|
this.channel.close();
|
||||||
|
|
||||||
@@ -161,7 +171,7 @@ public class GameClient {
|
|||||||
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
|
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
|
||||||
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
|
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
|
||||||
// Try to park the habbo in the grace period instead of immediate disconnect
|
// Try to park the habbo in the grace period instead of immediate disconnect
|
||||||
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
|
boolean parked = allowSessionResume && SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
|
||||||
|
|
||||||
if (!parked) {
|
if (!parked) {
|
||||||
// No grace period configured — immediate disconnect as before
|
// No grace period configured — immediate disconnect as before
|
||||||
|
|||||||
@@ -43,14 +43,34 @@ public class GameClientManager {
|
|||||||
|
|
||||||
|
|
||||||
public void disposeClient(GameClient client) {
|
public void disposeClient(GameClient client) {
|
||||||
this.disposeClient(client.getChannel());
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disposeClient(client.getChannel(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceDisposeClient(GameClient client) {
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disposeClient(client.getChannel(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disposeClient(Channel channel) {
|
private void disposeClient(Channel channel) {
|
||||||
|
this.disposeClient(channel, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disposeClient(Channel channel, boolean allowSessionResume) {
|
||||||
|
if (channel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
|
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
|
||||||
|
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.dispose();
|
client.dispose(allowSessionResume);
|
||||||
}
|
}
|
||||||
channel.deregister();
|
channel.deregister();
|
||||||
channel.attr(GameServerAttributes.CLIENT).set(null);
|
channel.attr(GameServerAttributes.CLIENT).set(null);
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ public class SessionResumeManager {
|
|||||||
}
|
}
|
||||||
}, graceSeconds * 1000);
|
}, graceSeconds * 1000);
|
||||||
|
|
||||||
|
if (future == null) {
|
||||||
|
// The scheduler refused the grace-expiry task (pool saturated or
|
||||||
|
// shutting down). Parking now would leave a GhostSession that nothing
|
||||||
|
// can ever reap (the Habbo + room refs pinned for the JVM lifetime),
|
||||||
|
// so disconnect immediately instead.
|
||||||
|
performFullDisconnect(habbo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
||||||
|
|
||||||
applyPausedEffect(habbo);
|
applyPausedEffect(habbo);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.eu.habbo.habbohotel.guilds;
|
||||||
|
|
||||||
|
import com.eu.habbo.messages.ClientMessage;
|
||||||
|
import com.eu.habbo.util.PacketGuard;
|
||||||
|
|
||||||
|
public final class GuildBadgeBuilder {
|
||||||
|
public static final int MAX_BADGE_PARTS = 5;
|
||||||
|
private static final int INTS_PER_PART = 3;
|
||||||
|
private static final int BYTES_PER_INT = 4;
|
||||||
|
private static final int MAX_PART_ID = 999;
|
||||||
|
private static final int MAX_COLOR_ID = 99;
|
||||||
|
private static final int MAX_POSITION = 8;
|
||||||
|
|
||||||
|
private GuildBadgeBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readBadge(ClientMessage packet, int flatPartValueCount) {
|
||||||
|
if (flatPartValueCount % INTS_PER_PART != 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int partCount = flatPartValueCount / INTS_PER_PART;
|
||||||
|
if (!PacketGuard.isCountInRange(partCount, 1, MAX_BADGE_PARTS)
|
||||||
|
|| !PacketGuard.hasFixedWidthEntries(packet, flatPartValueCount, BYTES_PER_INT)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder badge = new StringBuilder(partCount * 6);
|
||||||
|
for (int partIndex = 0; partIndex < partCount; partIndex++) {
|
||||||
|
int id = packet.readInt();
|
||||||
|
int color = packet.readInt();
|
||||||
|
int position = packet.readInt();
|
||||||
|
|
||||||
|
if (!isValidPart(id, color, position)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
badge.append(partIndex == 0 ? "b" : "s");
|
||||||
|
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id);
|
||||||
|
badge.append(color < 10 ? "0" : "").append(color);
|
||||||
|
badge.append(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return badge.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidPart(int id, int color, int position) {
|
||||||
|
return id >= 0 && id <= MAX_PART_ID
|
||||||
|
&& color >= 0 && color <= MAX_COLOR_ID
|
||||||
|
&& position >= 0 && position <= MAX_POSITION;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -291,11 +291,12 @@ public class GuildManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!error) {
|
} else if (!error) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ?")) {
|
try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ? AND level_id = ?")) {
|
||||||
statement.setInt(1, GuildRank.MEMBER.type);
|
statement.setInt(1, GuildRank.MEMBER.type);
|
||||||
statement.setInt(2, Emulator.getIntUnixTimestamp());
|
statement.setInt(2, Emulator.getIntUnixTimestamp());
|
||||||
statement.setInt(3, userId);
|
statement.setInt(3, userId);
|
||||||
statement.setInt(4, guild.getId());
|
statement.setInt(4, guild.getId());
|
||||||
|
statement.setInt(5, GuildRank.REQUESTED.type);
|
||||||
statement.execute();
|
statement.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,9 +422,9 @@ public class GuildManager {
|
|||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
||||||
statement.setInt(1, guild.getId());
|
statement.setInt(1, guild.getId());
|
||||||
statement.setString(2, "%" + query + "%");
|
statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||||
statement.setInt(3, page * 14);
|
statement.setInt(3, page * 14);
|
||||||
statement.setInt(4, (page * 14) + 14);
|
statement.setInt(4, 14);
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
while (set.next()) {
|
while (set.next()) {
|
||||||
@@ -440,7 +441,7 @@ public class GuildManager {
|
|||||||
public int getGuildMembersCount(Guild guild, int page, int levelId, String query) {
|
public int getGuildMembersCount(Guild guild, int page, int levelId, String query) {
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC")) {
|
||||||
statement.setInt(1, guild.getId());
|
statement.setInt(1, guild.getId());
|
||||||
statement.setString(2, "%" + query + "%");
|
statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
while (set.next()) {
|
while (set.next()) {
|
||||||
|
|||||||
@@ -101,21 +101,27 @@ public class ForumThread implements Runnable, ISerialize {
|
|||||||
if (statement.executeUpdate() < 1)
|
if (statement.executeUpdate() < 1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ResultSet set = statement.getGeneratedKeys();
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
int threadId = set.getInt(1);
|
int threadId = set.getInt(1);
|
||||||
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
||||||
cacheThread(createdThread);
|
cacheThread(createdThread);
|
||||||
|
}
|
||||||
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
|
|
||||||
createdThread.addComment(comment);
|
|
||||||
|
|
||||||
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
|
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForumThreadComment.create() opens its OWN connection; do it after the
|
||||||
|
// thread's connection has been released to avoid holding two pooled
|
||||||
|
// connections simultaneously per forum-thread creation.
|
||||||
|
if (createdThread != null) {
|
||||||
|
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
|
||||||
|
createdThread.addComment(comment);
|
||||||
|
|
||||||
|
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
|
||||||
|
}
|
||||||
|
|
||||||
return createdThread;
|
return createdThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -98,12 +98,13 @@ public class ForumThreadComment implements Runnable, ISerialize {
|
|||||||
if (statement.executeUpdate() < 1)
|
if (statement.executeUpdate() < 1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ResultSet set = statement.getGeneratedKeys();
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
int commentId = set.getInt(1);
|
int commentId = set.getInt(1);
|
||||||
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
|
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
|
||||||
|
|
||||||
Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
|
Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One parsed furnidata entry. {@code classname} is the raw furnidata classname
|
||||||
|
* (may carry a {@code *N} colour-variant suffix); the provider keys on the base.
|
||||||
|
*/
|
||||||
|
public record FurnidataEntry(int id, String classname, FurnitureType type, String name, String description) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a complete furnidata entry object (single-line JSON5) from an {@link Item}
|
||||||
|
* (its items_base row) plus a display name/description. Used by the Furni Editor
|
||||||
|
* upsert path when a furni has no furnidata entry yet. Field shape mirrors the
|
||||||
|
* hotel's existing furnidata entries; {@code id} is the item's sprite id so the
|
||||||
|
* renderer resolves the furni's name/data by typeId.
|
||||||
|
*/
|
||||||
|
public final class FurnidataEntryBuilder {
|
||||||
|
|
||||||
|
private FurnidataEntryBuilder() {}
|
||||||
|
|
||||||
|
public static String build(Item item, String name, String description) {
|
||||||
|
String classname = item.getName() != null ? item.getName() : "";
|
||||||
|
String safeName = (name != null && !name.isBlank()) ? name
|
||||||
|
: (item.getFullName() != null && !item.getFullName().isBlank()) ? item.getFullName()
|
||||||
|
: classname;
|
||||||
|
String safeDesc = description != null ? description : "";
|
||||||
|
String customParams = item.getCustomParams() != null ? item.getCustomParams() : "";
|
||||||
|
|
||||||
|
StringBuilder b = new StringBuilder(256);
|
||||||
|
b.append("{\"id\":").append(item.getSpriteId());
|
||||||
|
b.append(",\"classname\":\"").append(esc(classname)).append('"');
|
||||||
|
b.append(",\"revision\":0,\"category\":\"unknown\",\"defaultdir\":0");
|
||||||
|
b.append(",\"xdim\":").append(item.getWidth());
|
||||||
|
b.append(",\"ydim\":").append(item.getLength());
|
||||||
|
b.append(",\"partcolors\":{\"color\":[]}");
|
||||||
|
b.append(",\"name\":\"").append(esc(safeName)).append('"');
|
||||||
|
b.append(",\"description\":\"").append(esc(safeDesc)).append('"');
|
||||||
|
b.append(",\"adurl\":\"\",\"offerid\":-1,\"buyout\":false,\"rentofferid\":-1,\"rentbuyout\":false,\"bc\":false,\"excludeddynamic\":false");
|
||||||
|
b.append(",\"customparams\":\"").append(esc(customParams)).append('"');
|
||||||
|
b.append(",\"specialtype\":1");
|
||||||
|
b.append(",\"canstandon\":").append(item.allowWalk());
|
||||||
|
b.append(",\"cansiton\":").append(item.allowSit());
|
||||||
|
b.append(",\"canlayon\":").append(item.allowLay());
|
||||||
|
b.append('}');
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Escape for a JSON string value; collapse control chars to spaces. */
|
||||||
|
private static String esc(String v) {
|
||||||
|
StringBuilder b = new StringBuilder(v.length() + 8);
|
||||||
|
for (int i = 0; i < v.length(); i++) {
|
||||||
|
char c = v.charAt(i);
|
||||||
|
if (c == '"' || c == '\\') b.append('\\').append(c);
|
||||||
|
else if (c == '\n' || c == '\r' || c == '\t') b.append(' ');
|
||||||
|
else b.append(c);
|
||||||
|
}
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One process-wide lock serializing every furnidata reindex and every editor-driven
|
||||||
|
* furnidata write, so an editor write never races the file watcher's reindex and the
|
||||||
|
* volatile index is never observed mid-swap by two writers.
|
||||||
|
*/
|
||||||
|
public final class FurnidataLock {
|
||||||
|
public static final ReentrantLock LOCK = new ReentrantLock();
|
||||||
|
private FurnidataLock() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neutral furnidata reader. Supports a single JSON/JSON5 file or a split-tier
|
||||||
|
* directory ({@code core/custom/seasonal} with {@code manifest.json(5)}).
|
||||||
|
* Never throws: any IO/parse error yields an empty list (the caller decides the
|
||||||
|
* fallback). All resolved paths are guarded against escaping the base dir.
|
||||||
|
*/
|
||||||
|
public class FurnidataReader {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnidataReader.class);
|
||||||
|
private static final List<String> DEFAULT_TIERS = Arrays.asList("core", "custom", "seasonal");
|
||||||
|
private static final List<String> MANIFEST_NAMES = Arrays.asList("manifest.json5", "manifest.json");
|
||||||
|
private static final List<String> SECTIONS = Arrays.asList("roomitemtypes", "wallitemtypes");
|
||||||
|
|
||||||
|
private final Path source;
|
||||||
|
private final long maxBytes;
|
||||||
|
|
||||||
|
public FurnidataReader(Path source, long maxBytes) {
|
||||||
|
this.source = source;
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FurnidataEntry> read() {
|
||||||
|
List<FurnidataEntry> out = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
if (this.source == null || !Files.exists(this.source)) return out;
|
||||||
|
|
||||||
|
if (Files.isDirectory(this.source)) {
|
||||||
|
readSplitDir(this.source, out);
|
||||||
|
} else {
|
||||||
|
String content = readJson5Capped(this.source);
|
||||||
|
if (content != null) {
|
||||||
|
parseRoot(JsonParser.parseString(content).getAsJsonObject(), out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataReader failed to read {} — returning empty", this.source, e);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readSplitDir(Path base, List<FurnidataEntry> out) {
|
||||||
|
List<String> tiers = readManifestList(base, "tiers", DEFAULT_TIERS);
|
||||||
|
Path baseNorm = base.toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
for (String tier : tiers) {
|
||||||
|
Path tierDir = base.resolve(tier);
|
||||||
|
if (!isInside(baseNorm, tierDir) || !Files.isDirectory(tierDir)) continue;
|
||||||
|
|
||||||
|
for (String fileName : readManifestList(tierDir, "files", List.of())) {
|
||||||
|
Path file = tierDir.resolve(fileName);
|
||||||
|
if (!isInside(baseNorm, file)) {
|
||||||
|
LOGGER.warn("FurnidataReader: ignoring out-of-base file {}", file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Files.exists(file)) continue;
|
||||||
|
try {
|
||||||
|
String content = readJson5Capped(file);
|
||||||
|
if (content != null) parseRoot(JsonParser.parseString(content).getAsJsonObject(), out);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataReader: failed to parse {}", file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> readManifestList(Path dir, String key, List<String> fallback) {
|
||||||
|
for (String name : MANIFEST_NAMES) {
|
||||||
|
Path m = dir.resolve(name);
|
||||||
|
if (!Files.exists(m)) continue;
|
||||||
|
try {
|
||||||
|
String raw = readJson5Capped(m);
|
||||||
|
if (raw == null) continue;
|
||||||
|
JsonObject obj = JsonParser.parseString(raw).getAsJsonObject();
|
||||||
|
if (obj.has(key) && obj.get(key).isJsonArray()) {
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (JsonElement el : obj.getAsJsonArray(key)) list.add(el.getAsString());
|
||||||
|
if (!list.isEmpty()) return list;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataReader: bad manifest {}", m, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseRoot(JsonObject root, List<FurnidataEntry> out) {
|
||||||
|
for (String section : SECTIONS) {
|
||||||
|
if (!root.has(section)) continue;
|
||||||
|
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||||
|
if (!sectionObj.has("furnitype")) continue;
|
||||||
|
FurnitureType type = section.equals("roomitemtypes") ? FurnitureType.FLOOR : FurnitureType.WALL;
|
||||||
|
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||||
|
for (JsonElement el : types) {
|
||||||
|
JsonObject o = el.getAsJsonObject();
|
||||||
|
if (!o.has("id") || o.get("id").isJsonNull() || !o.has("classname") || o.get("classname").isJsonNull()) continue;
|
||||||
|
out.add(new FurnidataEntry(
|
||||||
|
o.get("id").getAsInt(),
|
||||||
|
o.get("classname").getAsString(),
|
||||||
|
type,
|
||||||
|
(o.has("name") && !o.get("name").isJsonNull()) ? o.get("name").getAsString() : "",
|
||||||
|
(o.has("description") && !o.get("description").isJsonNull()) ? o.get("description").getAsString() : ""
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the JSON5-stripped content, or null if the file exceeds the byte cap. */
|
||||||
|
private String readJson5Capped(Path path) throws Exception {
|
||||||
|
long size = Files.size(path);
|
||||||
|
if (size > this.maxBytes) {
|
||||||
|
LOGGER.warn("FurnidataReader: {} is {} bytes, over cap {} — refusing", path, size, this.maxBytes);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return stripJson5(Files.readString(path, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isInside(Path baseNorm, Path candidate) {
|
||||||
|
return candidate.toAbsolutePath().normalize().startsWith(baseNorm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip // and block comments and trailing commas so Gson can parse JSON5.
|
||||||
|
* Known limitation: the trailing-comma pass is a regex over the whole output,
|
||||||
|
* so a string value literally containing ",[whitespace]}" or ",[whitespace]]"
|
||||||
|
* would be altered. Real Habbo furnidata names/descriptions do not contain
|
||||||
|
* that pattern; values are additionally sanitized downstream before use.
|
||||||
|
*/
|
||||||
|
static String stripJson5(String content) {
|
||||||
|
if (content == null || content.isEmpty()) return content;
|
||||||
|
StringBuilder out = new StringBuilder(content.length());
|
||||||
|
int i = 0, len = content.length();
|
||||||
|
boolean inString = false, escape = false;
|
||||||
|
char stringChar = 0;
|
||||||
|
while (i < len) {
|
||||||
|
char c = content.charAt(i);
|
||||||
|
if (inString) {
|
||||||
|
out.append(c);
|
||||||
|
if (escape) escape = false;
|
||||||
|
else if (c == '\\') escape = true;
|
||||||
|
else if (c == stringChar) inString = false;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '"' || c == '\'') { inString = true; stringChar = c; out.append(c); i++; continue; }
|
||||||
|
if (c == '/' && i + 1 < len) {
|
||||||
|
char next = content.charAt(i + 1);
|
||||||
|
if (next == '/') { int eol = content.indexOf('\n', i + 2); if (eol < 0) break; i = eol; continue; }
|
||||||
|
if (next == '*') { int end = content.indexOf("*/", i + 2); if (end < 0) break; i = end + 2; continue; }
|
||||||
|
}
|
||||||
|
out.append(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return out.toString().replaceAll(",(\\s*[}\\]])", "$1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public final class FurnidataSourceResolver {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnidataSourceResolver.class);
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
RESOLVED,
|
||||||
|
SOURCE_MISSING,
|
||||||
|
CONFIG_MISSING,
|
||||||
|
UNRESOLVED_PLACEHOLDER,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Source(Path path, boolean directory, Status status, String message) {
|
||||||
|
public boolean ok() {
|
||||||
|
return this.status == Status.RESOLVED && this.path != null && Files.exists(this.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FurnidataSourceResolver() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Source resolve() {
|
||||||
|
try {
|
||||||
|
String override = Emulator.getConfig().getValue("items.furnidata.path", "");
|
||||||
|
String rendererConfigPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
||||||
|
String assetBasePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||||
|
|
||||||
|
return resolveConfigured(override, rendererConfigPath, assetBasePath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataSourceResolver failed", e);
|
||||||
|
return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "Resolver error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Source resolveConfigured(String legacyOverridePath, String rendererConfigPath, String assetBasePath) {
|
||||||
|
if (rendererConfigPath != null && !rendererConfigPath.isEmpty()) {
|
||||||
|
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath == null || assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
|
||||||
|
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Source fromAssetBase = resolveFromAssetBase(assetBasePath);
|
||||||
|
if (fromAssetBase != null && fromAssetBase.ok()) return fromAssetBase;
|
||||||
|
|
||||||
|
if (legacyOverridePath != null && !legacyOverridePath.isEmpty()) {
|
||||||
|
Path p = Paths.get(legacyOverridePath);
|
||||||
|
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path fallback");
|
||||||
|
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path fallback does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromAssetBase != null) return fromAssetBase;
|
||||||
|
|
||||||
|
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Source resolveFromRendererConfig(Path rendererConfig, Path assetBase) {
|
||||||
|
try {
|
||||||
|
if (rendererConfig == null || !Files.exists(rendererConfig)) {
|
||||||
|
return new Source(rendererConfig, false, Status.SOURCE_MISSING, "renderer-config path does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
String raw = Files.readString(rendererConfig, StandardCharsets.UTF_8);
|
||||||
|
JsonObject rendererObj = JsonParser.parseString(FurnidataReader.stripJson5(raw)).getAsJsonObject();
|
||||||
|
String furniUrl = expandRendererUrl(rendererObj, "furnidata.url");
|
||||||
|
|
||||||
|
if (furniUrl.isBlank()) return new Source(null, false, Status.CONFIG_MISSING, "furnidata.url is missing");
|
||||||
|
if (hasUnresolvedPathPlaceholder(furniUrl)) return new Source(null, false, Status.UNRESOLVED_PLACEHOLDER, furniUrl);
|
||||||
|
|
||||||
|
Source source = toLocalSource(assetBase, furniUrl);
|
||||||
|
if (source == null) return new Source(null, false, Status.CONFIG_MISSING, "furni.editor.asset.base.path is missing");
|
||||||
|
if (!Files.exists(source.path())) return new Source(source.path(), source.directory(), Status.SOURCE_MISSING, "Resolved source does not exist");
|
||||||
|
|
||||||
|
return source;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "renderer-config parse failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Source resolveFromAssetBase(String assetBasePath) {
|
||||||
|
if (assetBasePath == null || assetBasePath.isEmpty()) return null;
|
||||||
|
|
||||||
|
Path dir = Paths.get(assetBasePath);
|
||||||
|
Path split = dir.resolve("furnidata");
|
||||||
|
if (Files.isDirectory(split)) return new Source(split, true, Status.RESOLVED, "asset base split furnidata");
|
||||||
|
|
||||||
|
Path legacy = dir.resolve("FurnitureData.json");
|
||||||
|
if (Files.exists(legacy)) return new Source(legacy, false, Status.RESOLVED, "asset base FurnitureData.json");
|
||||||
|
|
||||||
|
return new Source(dir, true, Status.SOURCE_MISSING, "No furnidata or FurnitureData.json under asset base");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String expandRendererUrl(JsonObject rendererObj, String key) {
|
||||||
|
if (rendererObj == null || !rendererObj.has(key)) return "";
|
||||||
|
|
||||||
|
String value = rendererObj.get(key).getAsString();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
int start = value.indexOf("${");
|
||||||
|
if (start < 0) break;
|
||||||
|
|
||||||
|
int end = value.indexOf('}', start + 2);
|
||||||
|
if (end < 0) break;
|
||||||
|
|
||||||
|
String placeholder = value.substring(start + 2, end);
|
||||||
|
if (!rendererObj.has(placeholder)) break;
|
||||||
|
|
||||||
|
value = value.substring(0, start) + rendererObj.get(placeholder).getAsString() + value.substring(end + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Source toLocalSource(Path assetBase, String furniUrl) {
|
||||||
|
if (furniUrl == null || furniUrl.isBlank()) return null;
|
||||||
|
|
||||||
|
String cleanUrl = stripQueryAndFragment(furniUrl);
|
||||||
|
boolean splitMode = cleanUrl.endsWith("/");
|
||||||
|
|
||||||
|
if (!cleanUrl.startsWith("http")) {
|
||||||
|
Path local = Paths.get(cleanUrl);
|
||||||
|
return new Source(local, splitMode || Files.isDirectory(local), Status.RESOLVED, "local furnidata.url");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetBase == null) return null;
|
||||||
|
|
||||||
|
String urlPath;
|
||||||
|
try {
|
||||||
|
urlPath = URI.create(cleanUrl).getPath();
|
||||||
|
} catch (Exception e) {
|
||||||
|
int scheme = cleanUrl.indexOf("://");
|
||||||
|
int pathStart = scheme >= 0 ? cleanUrl.indexOf('/', scheme + 3) : -1;
|
||||||
|
urlPath = pathStart >= 0 ? cleanUrl.substring(pathStart) : cleanUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = urlPath.replace('\\', '/');
|
||||||
|
String baseName = assetBase.getFileName() != null ? assetBase.getFileName().toString() : "";
|
||||||
|
String marker = "/" + baseName + "/";
|
||||||
|
int markerIndex = baseName.isEmpty() ? -1 : normalized.indexOf(marker);
|
||||||
|
|
||||||
|
Path candidate;
|
||||||
|
if (markerIndex >= 0) {
|
||||||
|
candidate = assetBase.resolve(normalized.substring(markerIndex + marker.length()));
|
||||||
|
} else if (splitMode) {
|
||||||
|
String trimmed = normalized.endsWith("/") ? normalized.substring(0, normalized.length() - 1) : normalized;
|
||||||
|
candidate = assetBase.resolve(trimmed.substring(trimmed.lastIndexOf('/') + 1));
|
||||||
|
} else {
|
||||||
|
candidate = assetBase.resolve(normalized.substring(normalized.lastIndexOf('/') + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Source(candidate, splitMode || Files.isDirectory(candidate), Status.RESOLVED, "renderer-config furnidata.url");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasUnresolvedPathPlaceholder(String value) {
|
||||||
|
if (value == null) return false;
|
||||||
|
return stripQueryAndFragment(value).contains("${");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripQueryAndFragment(String value) {
|
||||||
|
String out = value;
|
||||||
|
int q = out.indexOf('?');
|
||||||
|
if (q >= 0) out = out.substring(0, q);
|
||||||
|
int h = out.indexOf('#');
|
||||||
|
if (h >= 0) out = out.substring(0, h);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
import com.eu.habbo.messages.outgoing.furniture.FurnitureDataReloadComposer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.ClosedWatchServiceException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardWatchEventKinds;
|
||||||
|
import java.nio.file.WatchKey;
|
||||||
|
import java.nio.file.WatchService;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches the furnidata source on a single daemon thread. On change (debounced),
|
||||||
|
* re-indexes via the provider and broadcasts only the delta — or a compact
|
||||||
|
* reload-hint when the delta exceeds the cap. A minimum interval throttles bursts.
|
||||||
|
* For the split-tier directory layout, the base dir AND its immediate
|
||||||
|
* subdirectories are registered. Never throws out of the loop.
|
||||||
|
*/
|
||||||
|
public class FurnidataWatcher {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnidataWatcher.class);
|
||||||
|
|
||||||
|
private final FurnitureTextProvider provider;
|
||||||
|
private final Path watchDir;
|
||||||
|
private final boolean sourceIsDir;
|
||||||
|
private final long maxBytes;
|
||||||
|
private final long debounceMs;
|
||||||
|
private final long minIntervalMs;
|
||||||
|
private final int deltaCap;
|
||||||
|
|
||||||
|
private volatile boolean running = false;
|
||||||
|
private volatile WatchService ws;
|
||||||
|
private long lastBroadcast = 0L;
|
||||||
|
|
||||||
|
public FurnidataWatcher(FurnitureTextProvider provider, Path source, long maxBytes) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.sourceIsDir = Files.isDirectory(source);
|
||||||
|
this.watchDir = this.sourceIsDir ? source : source.getParent();
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
this.debounceMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.debounce.ms", "750"));
|
||||||
|
this.minIntervalMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.min.interval.ms", "5000"));
|
||||||
|
this.deltaCap = Integer.parseInt(Emulator.getConfig().getValue("items.furnidata.delta.cap", "500"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (this.running || this.watchDir == null) return;
|
||||||
|
this.running = true;
|
||||||
|
Thread t = new Thread(this::run, "FurnidataWatcher");
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
this.running = false;
|
||||||
|
WatchService local = this.ws;
|
||||||
|
if (local != null) {
|
||||||
|
try { local.close(); } catch (IOException ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run() {
|
||||||
|
try {
|
||||||
|
this.ws = FileSystems.getDefault().newWatchService();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("FurnidataWatcher: could not create WatchService", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (WatchService service = this.ws) {
|
||||||
|
registerDirs(service);
|
||||||
|
while (this.running) {
|
||||||
|
WatchKey key = service.take();
|
||||||
|
key.pollEvents();
|
||||||
|
Thread.sleep(this.debounceMs);
|
||||||
|
key.pollEvents();
|
||||||
|
if (!key.reset()) {
|
||||||
|
LOGGER.warn("FurnidataWatcher: watch key invalidated (directory removed?) — stopping");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
onChange();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataWatcher: onChange failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (ClosedWatchServiceException ignored) {
|
||||||
|
// stop() closed the service — normal shutdown
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataWatcher stopped", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register the base dir, plus one level of subdirectories for the split-tier layout. */
|
||||||
|
private void registerDirs(WatchService service) throws IOException {
|
||||||
|
this.watchDir.register(service, StandardWatchEventKinds.ENTRY_MODIFY,
|
||||||
|
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
|
||||||
|
if (this.sourceIsDir) {
|
||||||
|
try (DirectoryStream<Path> ds = Files.newDirectoryStream(this.watchDir)) {
|
||||||
|
for (Path child : ds) {
|
||||||
|
if (Files.isDirectory(child)) {
|
||||||
|
child.register(service, StandardWatchEventKinds.ENTRY_MODIFY,
|
||||||
|
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onChange() throws InterruptedException {
|
||||||
|
// Re-index under the shared furnidata lock so the watcher and editor
|
||||||
|
// writes never swap the index concurrently. The lock is released before
|
||||||
|
// the throttle/broadcast below so a slow broadcast can't stall editor saves.
|
||||||
|
List<FurnidataEntry> delta;
|
||||||
|
FurnidataLock.LOCK.lock();
|
||||||
|
try {
|
||||||
|
Path source = this.provider.getSource();
|
||||||
|
if (source == null) return;
|
||||||
|
delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
|
||||||
|
} finally {
|
||||||
|
FurnidataLock.LOCK.unlock();
|
||||||
|
}
|
||||||
|
if (delta.isEmpty()) return;
|
||||||
|
|
||||||
|
// Min-interval throttle: the index has already been swapped, so we must
|
||||||
|
// not drop this delta (the next reindex would diff against the updated
|
||||||
|
// index and never re-emit it). Instead, defer the broadcast until the
|
||||||
|
// interval elapses. Running on a dedicated daemon thread, sleeping is
|
||||||
|
// safe; file events arriving meanwhile coalesce into the next cycle.
|
||||||
|
long sinceLast = System.currentTimeMillis() - this.lastBroadcast;
|
||||||
|
if (sinceLast < this.minIntervalMs) {
|
||||||
|
Thread.sleep(this.minIntervalMs - sinceLast);
|
||||||
|
}
|
||||||
|
this.lastBroadcast = System.currentTimeMillis();
|
||||||
|
|
||||||
|
FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
|
||||||
|
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
|
||||||
|
: new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
|
||||||
|
|
||||||
|
broadcast(composer);
|
||||||
|
LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
|
||||||
|
delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcast(FurnitureDataReloadComposer composer) {
|
||||||
|
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
|
||||||
|
if (habbo.getClient() != null) {
|
||||||
|
habbo.getClient().sendResponse(composer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comment-preserving, atomic, backed-up writer for furnidata name/description, keyed by
|
||||||
|
* classname. Supports single-file and split-tier (writes the tier that currently resolves
|
||||||
|
* the classname). Edit-only: refuses classnames absent from the furnidata.
|
||||||
|
*/
|
||||||
|
public class FurnidataWriter {
|
||||||
|
|
||||||
|
/** Default tier names in override order (later = higher priority, wins on conflict). */
|
||||||
|
private static final List<String> DEFAULT_TIERS = Arrays.asList("core", "custom", "seasonal");
|
||||||
|
|
||||||
|
/** Manifest filenames tried in order (json5 first, plain json second). */
|
||||||
|
private static final List<String> MANIFEST_NAMES = Arrays.asList("manifest.json5", "manifest.json");
|
||||||
|
|
||||||
|
private final Path source; // file (single) or base dir (split-tier)
|
||||||
|
private final boolean directory; // true => split-tier
|
||||||
|
private final long maxBytes;
|
||||||
|
private final int backupKeep;
|
||||||
|
|
||||||
|
public FurnidataWriter(Path source, boolean directory, long maxBytes, int backupKeep) {
|
||||||
|
this.source = source;
|
||||||
|
this.directory = directory;
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
this.backupKeep = Math.max(1, backupKeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true if an entry for classname was found and written. */
|
||||||
|
public boolean write(String classname, String name, String description) throws IOException {
|
||||||
|
String cn = classname == null ? "" : classname.trim().toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (cn.isEmpty()) return false;
|
||||||
|
String safeName = FurnitureTextProvider.sanitize(name);
|
||||||
|
String safeDesc = FurnitureTextProvider.sanitize(description);
|
||||||
|
|
||||||
|
Path target = locateFile(cn);
|
||||||
|
if (target == null) return false;
|
||||||
|
|
||||||
|
String raw = Files.readString(target, StandardCharsets.UTF_8);
|
||||||
|
String edited = replaceEntryFields(raw, cn, safeName, safeDesc);
|
||||||
|
if (edited == null || edited.equals(raw)) {
|
||||||
|
// classname not present in this file, or no change
|
||||||
|
return edited != null && !edited.equals(raw);
|
||||||
|
}
|
||||||
|
backup(target);
|
||||||
|
atomicWrite(target, edited);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Outcome of a {@link #create} attempt. */
|
||||||
|
public enum CreateResult { CREATED, ALREADY_EXISTS, ID_COLLISION, NO_TARGET, IO_ERROR }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a brand-new furnidata entry (upsert's "create" half). Refuses if the
|
||||||
|
* classname already exists (caller should edit instead) or if {@code id} is
|
||||||
|
* already used by a DIFFERENT classname (id collision would break the
|
||||||
|
* {@code roomItem.name.<id>} / typeId resolution on the renderer). The complete
|
||||||
|
* entry object is built by the caller (see FurnidataEntryBuilder) and inserted
|
||||||
|
* right after the opening '[' of the matching section's "furnitype" array.
|
||||||
|
*
|
||||||
|
* @param classname new classname (must be absent from furnidata)
|
||||||
|
* @param id furnidata id (= item sprite id); must not collide
|
||||||
|
* @param type FLOOR -> roomitemtypes, WALL -> wallitemtypes
|
||||||
|
* @param entryJson5 the complete entry object as a single-line JSON5 string
|
||||||
|
* @param createTier split-tier only: the tier dir to write into (e.g. "custom"); ignored for single-file
|
||||||
|
*/
|
||||||
|
public CreateResult create(String classname, int id, FurnitureType type, String entryJson5, String createTier) {
|
||||||
|
String cn = classname == null ? "" : classname.trim().toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (cn.isEmpty() || entryJson5 == null || entryJson5.isBlank()) return CreateResult.NO_TARGET;
|
||||||
|
|
||||||
|
// Guard: duplicate classname / id collision (scan the whole source).
|
||||||
|
for (FurnidataEntry e : new FurnidataReader(source, maxBytes).read()) {
|
||||||
|
String ecn = e.classname() == null ? "" : e.classname().trim().toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (ecn.equals(cn)) return CreateResult.ALREADY_EXISTS;
|
||||||
|
if (e.id() == id) return CreateResult.ID_COLLISION;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path target = resolveCreateTarget(createTier);
|
||||||
|
if (target == null) return CreateResult.NO_TARGET;
|
||||||
|
|
||||||
|
String raw = Files.readString(target, StandardCharsets.UTF_8);
|
||||||
|
String section = (type == FurnitureType.WALL) ? "wallitemtypes" : "roomitemtypes";
|
||||||
|
int open = furnitypeArrayOpenIndex(raw, section);
|
||||||
|
if (open < 0) return CreateResult.NO_TARGET; // section/array absent in target file
|
||||||
|
|
||||||
|
String edited = raw.substring(0, open) + "\n" + entryJson5 + "," + raw.substring(open);
|
||||||
|
backup(target);
|
||||||
|
atomicWrite(target, edited);
|
||||||
|
return CreateResult.CREATED;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return CreateResult.IO_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Single-file: the source. Split-tier: the create-tier file (created with a shell if absent). */
|
||||||
|
private Path resolveCreateTarget(String createTier) throws IOException {
|
||||||
|
if (!directory) return source;
|
||||||
|
String tier = (createTier == null || createTier.isBlank()) ? "custom" : createTier.trim();
|
||||||
|
Path base = source.toAbsolutePath().normalize();
|
||||||
|
Path tierDir = safeResolve(base, tier);
|
||||||
|
if (tierDir == null) return null;
|
||||||
|
if (!Files.isDirectory(tierDir)) Files.createDirectories(tierDir);
|
||||||
|
for (String fileName : manifestList(tierDir, "files", List.of())) {
|
||||||
|
Path f = safeResolve(base, tierDir.resolve(fileName).toString());
|
||||||
|
if (f != null && Files.isRegularFile(f)) return f;
|
||||||
|
}
|
||||||
|
Path def = tierDir.resolve("furnidata.json5");
|
||||||
|
if (!Files.exists(def)) {
|
||||||
|
Files.writeString(def,
|
||||||
|
"{\n \"roomitemtypes\": { \"furnitype\": [\n] },\n \"wallitemtypes\": { \"furnitype\": [\n] }\n}\n",
|
||||||
|
StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Index just after the '[' that opens {@code <section>.furnitype}, or -1 if absent. String-aware. */
|
||||||
|
static int furnitypeArrayOpenIndex(String raw, String section) {
|
||||||
|
int s = indexOfKey(raw, section, 0);
|
||||||
|
if (s < 0) return -1;
|
||||||
|
int ft = indexOfKey(raw, "furnitype", s);
|
||||||
|
if (ft < 0) return -1;
|
||||||
|
boolean inStr = false; char q = 0;
|
||||||
|
for (int i = ft; i < raw.length(); i++) {
|
||||||
|
char c = raw.charAt(i);
|
||||||
|
if (inStr) { if (c == '\\') i++; else if (c == q) inStr = false; continue; }
|
||||||
|
if (c == '"' || c == '\'') { inStr = true; q = c; }
|
||||||
|
else if (c == '[') return i + 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** First occurrence of a quoted key ("key" or 'key') at/after {@code from}, or -1. */
|
||||||
|
private static int indexOfKey(String raw, String key, int from) {
|
||||||
|
int a = raw.indexOf("\"" + key + "\"", from);
|
||||||
|
int b = raw.indexOf("'" + key + "'", from);
|
||||||
|
if (a < 0) return b;
|
||||||
|
if (b < 0) return a;
|
||||||
|
return Math.min(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For single-file just returns the file; for split-tier, the tier file that contains cn. */
|
||||||
|
private Path locateFile(String cn) throws IOException {
|
||||||
|
if (!directory) {
|
||||||
|
// confirm existence via the reader (size-guarded, parses the same way)
|
||||||
|
return containsClassname(source, cn) ? source : null;
|
||||||
|
}
|
||||||
|
// split-tier: iterate tiers in OVERRIDE order (later tiers win); pick the last containing cn
|
||||||
|
Path winner = null;
|
||||||
|
for (Path tierFile : splitTierFilesInOrder()) {
|
||||||
|
if (containsClassname(tierFile, cn)) winner = tierFile;
|
||||||
|
}
|
||||||
|
return winner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsClassname(Path file, String cn) {
|
||||||
|
for (FurnidataEntry e : new FurnidataReader(file, maxBytes).read()) {
|
||||||
|
if (e.classname() != null && e.classname().trim().toLowerCase(java.util.Locale.ROOT).equals(cn)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the "name" and "description" string values inside the JSON object that holds
|
||||||
|
* "classname": "<cn>". Preserves everything else (comments, ordering, formatting).
|
||||||
|
* Handles double- and single-quoted JSON5 keys/values. Returns null if cn not found.
|
||||||
|
*/
|
||||||
|
static String replaceEntryFields(String raw, String cn, String name, String description) {
|
||||||
|
// find the classname value occurrence (case-insensitive on the value)
|
||||||
|
Pattern classProp = Pattern.compile(
|
||||||
|
"([\"'])classname\\1\\s*:\\s*([\"'])((?:\\\\.|(?!\\2).)*)\\2", Pattern.CASE_INSENSITIVE);
|
||||||
|
Matcher m = classProp.matcher(raw);
|
||||||
|
int objStart = -1, objEnd = -1;
|
||||||
|
while (m.find()) {
|
||||||
|
String val = m.group(3).trim().toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (!val.equals(cn)) continue;
|
||||||
|
// expand to the enclosing { ... }
|
||||||
|
objStart = lastUnbalancedBrace(raw, m.start());
|
||||||
|
objEnd = matchingClose(raw, objStart);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (objStart < 0 || objEnd < 0) return null;
|
||||||
|
String obj = raw.substring(objStart, objEnd + 1);
|
||||||
|
String newObj = replaceField(obj, "name", name);
|
||||||
|
newObj = replaceField(newObj, "description", description);
|
||||||
|
return raw.substring(0, objStart) + newObj + raw.substring(objEnd + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String replaceField(String obj, String field, String value) {
|
||||||
|
Pattern p = Pattern.compile(
|
||||||
|
"(([\"'])" + Pattern.quote(field) + "\\2\\s*:\\s*)([\"'])((?:\\\\.|(?!\\3).)*)\\3");
|
||||||
|
Matcher m = p.matcher(obj);
|
||||||
|
if (!m.find()) return obj; // field absent → leave object as-is
|
||||||
|
String replacement = m.group(1) + '"' + jsonEscape(value) + '"';
|
||||||
|
return obj.substring(0, m.start()) + replacement + obj.substring(m.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int lastUnbalancedBrace(String s, int from) {
|
||||||
|
int depth = 0;
|
||||||
|
for (int i = from; i >= 0; i--) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == '}') depth++;
|
||||||
|
else if (c == '{') { if (depth == 0) return i; depth--; }
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int matchingClose(String s, int open) {
|
||||||
|
int depth = 0; boolean inStr = false; char q = 0;
|
||||||
|
for (int i = open; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (inStr) { if (c == '\\') { i++; } else if (c == q) inStr = false; continue; }
|
||||||
|
if (c == '"' || c == '\'') { inStr = true; q = c; }
|
||||||
|
else if (c == '{') depth++;
|
||||||
|
else if (c == '}') { depth--; if (depth == 0) return i; }
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonEscape(String v) {
|
||||||
|
StringBuilder b = new StringBuilder(v.length() + 8);
|
||||||
|
for (int i = 0; i < v.length(); i++) {
|
||||||
|
char c = v.charAt(i);
|
||||||
|
if (c == '"' || c == '\\') b.append('\\').append(c);
|
||||||
|
else b.append(c);
|
||||||
|
}
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerate every data file reachable from the split-tier base directory, in
|
||||||
|
* override order (core → custom → seasonal, or the order declared in the top-level
|
||||||
|
* {@code manifest.json(5)}). Within each tier the per-tier manifest's {@code files}
|
||||||
|
* array determines the file order.
|
||||||
|
*
|
||||||
|
* <p>All resolved paths are checked against the normalised base directory via
|
||||||
|
* {@link #safeResolve}: any entry that would escape the base is silently skipped.
|
||||||
|
*
|
||||||
|
* @return ordered list of existing, in-bounds data files (earliest tier first).
|
||||||
|
*/
|
||||||
|
private List<Path> splitTierFilesInOrder() throws IOException {
|
||||||
|
Path base = source.toAbsolutePath().normalize();
|
||||||
|
List<String> tiers = manifestList(base, "tiers", DEFAULT_TIERS);
|
||||||
|
List<Path> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String tier : tiers) {
|
||||||
|
Path tierDir = safeResolve(base, tier);
|
||||||
|
if (tierDir == null || !Files.isDirectory(tierDir)) continue;
|
||||||
|
|
||||||
|
for (String fileName : manifestList(tierDir, "files", List.of())) {
|
||||||
|
Path file = safeResolve(base, tierDir.resolve(fileName).toString());
|
||||||
|
if (file == null || !Files.isRegularFile(file)) continue;
|
||||||
|
result.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve {@code entry} relative to {@code base} and verify the result stays
|
||||||
|
* inside {@code base} (path-traversal guard).
|
||||||
|
*
|
||||||
|
* @param base the normalised absolute base directory.
|
||||||
|
* @param entry a path string (may be relative or absolute, may contain {@code ..}).
|
||||||
|
* @return the normalised absolute path if it is inside {@code base}; {@code null} otherwise.
|
||||||
|
*/
|
||||||
|
private static Path safeResolve(Path base, String entry) {
|
||||||
|
try {
|
||||||
|
Path resolved = base.resolve(entry).toAbsolutePath().normalize();
|
||||||
|
return resolved.startsWith(base) ? resolved : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the {@code key} string-array from the first manifest file found in {@code dir}
|
||||||
|
* ({@code manifest.json5} then {@code manifest.json}). Falls back to {@code fallback}
|
||||||
|
* if no manifest exists or the key is absent/empty.
|
||||||
|
*/
|
||||||
|
private List<String> manifestList(Path dir, String key, List<String> fallback) {
|
||||||
|
for (String name : MANIFEST_NAMES) {
|
||||||
|
Path m = dir.resolve(name);
|
||||||
|
if (!Files.exists(m)) continue;
|
||||||
|
try {
|
||||||
|
String stripped = FurnidataReader.stripJson5(
|
||||||
|
Files.readString(m, StandardCharsets.UTF_8));
|
||||||
|
com.google.gson.JsonObject obj =
|
||||||
|
com.google.gson.JsonParser.parseString(stripped).getAsJsonObject();
|
||||||
|
if (obj.has(key) && obj.get(key).isJsonArray()) {
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (com.google.gson.JsonElement el : obj.getAsJsonArray(key))
|
||||||
|
list.add(el.getAsString());
|
||||||
|
if (!list.isEmpty()) return list;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// bad manifest → fall through to next candidate / fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void backup(Path target) throws IOException {
|
||||||
|
Path bak = target.resolveSibling(target.getFileName() + ".bak." + System.nanoTime());
|
||||||
|
Files.copy(target, bak, StandardCopyOption.COPY_ATTRIBUTES);
|
||||||
|
pruneBackups(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pruneBackups(Path target) throws IOException {
|
||||||
|
String prefix = target.getFileName() + ".bak.";
|
||||||
|
try (var stream = Files.list(target.getParent())) {
|
||||||
|
List<Path> baks = stream.filter(p -> p.getFileName().toString().startsWith(prefix))
|
||||||
|
.sorted(Comparator.comparingLong(p -> backupStamp(p))).toList();
|
||||||
|
for (int i = 0; i < baks.size() - backupKeep; i++) Files.deleteIfExists(baks.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long backupStamp(Path p) {
|
||||||
|
String s = p.getFileName().toString();
|
||||||
|
try { return Long.parseLong(s.substring(s.lastIndexOf('.') + 1)); } catch (Exception e) { return 0L; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void atomicWrite(Path target, String content) throws IOException {
|
||||||
|
Path tmp = target.resolveSibling(target.getFileName() + ".tmp." + System.nanoTime());
|
||||||
|
Files.writeString(tmp, content, StandardCharsets.UTF_8);
|
||||||
|
try {
|
||||||
|
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restore the most recent backup of the (single-file) target. @return true if restored. */
|
||||||
|
public boolean revertLastBackup() throws IOException {
|
||||||
|
if (directory) return revertSplitTier();
|
||||||
|
return revertFile(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean revertFile(Path target) throws IOException {
|
||||||
|
String prefix = target.getFileName() + ".bak.";
|
||||||
|
try (var stream = Files.list(target.getParent())) {
|
||||||
|
Path latest = stream.filter(p -> p.getFileName().toString().startsWith(prefix))
|
||||||
|
.max(Comparator.comparingLong(FurnidataWriter::backupStamp)).orElse(null);
|
||||||
|
if (latest == null) return false;
|
||||||
|
atomicWrite(target, Files.readString(latest, StandardCharsets.UTF_8));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean revertSplitTier() throws IOException {
|
||||||
|
boolean any = false;
|
||||||
|
for (Path f : splitTierFilesInOrder()) any |= revertFile(f);
|
||||||
|
return any;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory index of furnidata display names, keyed by the lowercased base
|
||||||
|
* classname (the {@code *N} colour-variant suffix is stripped). Read lazily by
|
||||||
|
* {@link Item#getDisplayName()}. Names are sanitized at index time.
|
||||||
|
*
|
||||||
|
* Thread-safety: the index is held behind a {@code volatile} reference; readers
|
||||||
|
* never block; {@link #reindex(List)} builds a fresh map and swaps it atomically.
|
||||||
|
*/
|
||||||
|
public class FurnitureTextProvider {
|
||||||
|
|
||||||
|
private static final int MAX_LEN = 256;
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnitureTextProvider.class);
|
||||||
|
private static final long DEFAULT_MAX_BYTES = 64L * 1024 * 1024;
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
private volatile Map<String, FurniText> index = Map.of();
|
||||||
|
private volatile Path source;
|
||||||
|
private volatile String sourceDescription = "unknown";
|
||||||
|
private FurnidataWatcher watcher;
|
||||||
|
|
||||||
|
public FurnitureTextProvider(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Production constructor: reads the enable toggle from config. */
|
||||||
|
public FurnitureTextProvider() {
|
||||||
|
this(Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.names.enabled", "true")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the furnidata source from config and build the initial index. Never throws. */
|
||||||
|
public void init() {
|
||||||
|
try {
|
||||||
|
this.source = resolveSource();
|
||||||
|
if (this.source == null) {
|
||||||
|
LOGGER.warn("FurnitureTextProvider: no furnidata source resolved - names fall back to public_name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read());
|
||||||
|
LOGGER.info("Furniture Text Provider -> Indexed! ({} names, source: {})", this.index.size(), this.sourceDescription);
|
||||||
|
|
||||||
|
if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) {
|
||||||
|
if (this.watcher != null) this.watcher.stop();
|
||||||
|
this.watcher = new FurnidataWatcher(this, this.source, DEFAULT_MAX_BYTES);
|
||||||
|
this.watcher.start();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnitureTextProvider.init failed — names fall back to public_name", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getSource() {
|
||||||
|
return this.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} when the resolved source is a directory (split-tier layout). */
|
||||||
|
public boolean isSourceDirectory() {
|
||||||
|
return this.source != null && Files.isDirectory(this.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the byte cap used when reading furnidata files. */
|
||||||
|
public long getMaxBytes() {
|
||||||
|
return Long.parseLong(com.eu.habbo.Emulator.getConfig().getValue("items.furnidata.max.bytes", String.valueOf(DEFAULT_MAX_BYTES)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-reads the furnidata from the current source and reindexes atomically.
|
||||||
|
* Returns the delta list (new/changed entries) from {@link #reindex(List)}.
|
||||||
|
* Never throws — returns an empty list when the source is unavailable.
|
||||||
|
*/
|
||||||
|
public java.util.List<FurnidataEntry> reindexFromSource() {
|
||||||
|
try {
|
||||||
|
if (this.source == null) return java.util.List.of();
|
||||||
|
return reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnitureTextProvider.reindexFromSource failed", e);
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveSource() {
|
||||||
|
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve();
|
||||||
|
if (source.ok()) {
|
||||||
|
this.sourceDescription = source.message();
|
||||||
|
return source.path();
|
||||||
|
}
|
||||||
|
LOGGER.warn("FurnitureTextProvider: no furnidata source resolved ({}) - {}", source.status(), source.message());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a fresh sanitized index, swap it in atomically, and return the
|
||||||
|
* changed/added entries (sanitized) as the delta versus the previous index.
|
||||||
|
*/
|
||||||
|
public java.util.List<FurnidataEntry> reindex(java.util.List<FurnidataEntry> entries) {
|
||||||
|
Map<String, FurniText> next = new HashMap<>(Math.max(16, entries.size() * 2));
|
||||||
|
for (FurnidataEntry e : entries) {
|
||||||
|
String key = baseKey(e.classname());
|
||||||
|
if (key == null) continue;
|
||||||
|
next.put(key, new FurniText(e.id(), e.type(), sanitize(e.name()), sanitize(e.description())));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, FurniText> prev = this.index;
|
||||||
|
java.util.List<FurnidataEntry> delta = new java.util.ArrayList<>();
|
||||||
|
for (Map.Entry<String, FurniText> en : next.entrySet()) {
|
||||||
|
FurniText cur = en.getValue();
|
||||||
|
FurniText old = prev.get(en.getKey());
|
||||||
|
if (old == null || !old.name().equals(cur.name()) || !old.description().equals(cur.description())) {
|
||||||
|
delta.add(new FurnidataEntry(cur.id(), en.getKey(), cur.type(), cur.name(), cur.description()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.index = next; // atomic reference swap
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the sanitized display name for a DB classname, or null if absent/disabled. */
|
||||||
|
public String getName(String classname) {
|
||||||
|
if (!this.enabled) return null;
|
||||||
|
String key = baseKey(classname);
|
||||||
|
if (key == null) return null;
|
||||||
|
FurniText t = this.index.get(key);
|
||||||
|
return (t != null) ? t.name() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String baseKey(String classname) {
|
||||||
|
if (classname == null) return null;
|
||||||
|
int star = classname.indexOf('*');
|
||||||
|
String base = (star >= 0) ? classname.substring(0, star) : classname;
|
||||||
|
base = base.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return base.isEmpty() ? null : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cap length, strip control chars/newlines, neutralize % (placeholder-injection safe).
|
||||||
|
* The 256 cap is in Java {@code char} units (UTF-16 code units), which is acceptable for
|
||||||
|
* furni names (controlled, predominantly ASCII source). Lone/astral surrogates are not
|
||||||
|
* specially handled.
|
||||||
|
*/
|
||||||
|
public static String sanitize(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder(Math.min(value.length(), MAX_LEN));
|
||||||
|
for (int i = 0; i < value.length() && sb.length() < MAX_LEN; i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
if (c == '%') { sb.append('%'); continue; } // fullwidth percent — not a placeholder token
|
||||||
|
if (c == '\n' || c == '\r' || Character.isISOControl(c)) continue;
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all lowercased base classnames whose furnidata display name contains
|
||||||
|
* {@code query} (case-insensitive, substring). Results are capped at 200 to
|
||||||
|
* bound SQL IN-clause size. Returns an empty list when query is null/blank.
|
||||||
|
*/
|
||||||
|
public java.util.List<String> findClassnamesByName(String query) {
|
||||||
|
java.util.List<String> out = new java.util.ArrayList<>();
|
||||||
|
if (query == null) return out;
|
||||||
|
String q = query.trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (q.isEmpty()) return out;
|
||||||
|
Map<String, FurniText> idx = this.index; // local ref (volatile)
|
||||||
|
for (Map.Entry<String, FurniText> e : idx.entrySet()) {
|
||||||
|
FurniText t = e.getValue();
|
||||||
|
if (t != null && t.name() != null && t.name().toLowerCase(Locale.ROOT).contains(q)) {
|
||||||
|
out.add(e.getKey()); // key is the lowercased base classname
|
||||||
|
if (out.size() >= 200) break; // bound IN-clause size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record FurniText(int id, FurnitureType type, String name, String description) {}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class Item implements ISerialize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isPet(Item item) {
|
public static boolean isPet(Item item) {
|
||||||
return item.getName().toLowerCase().startsWith("a0 pet");
|
return item != null && item.getName() != null && item.getName().toLowerCase().startsWith("a0 pet");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isBot(Item item) {
|
public static boolean isBot(Item item) {
|
||||||
@@ -121,26 +121,19 @@ public class Item implements ISerialize {
|
|||||||
this.customParams = set.getString("customparams");
|
this.customParams = set.getString("customparams");
|
||||||
this.clothingOnWalk = set.getString("clothing_on_walk");
|
this.clothingOnWalk = set.getString("clothing_on_walk");
|
||||||
|
|
||||||
if (!set.getString("vending_ids").isEmpty()) {
|
int[] vendingIds = ItemDataGuard.parsePositiveIntList(set.getString("vending_ids"));
|
||||||
|
if (vendingIds.length > 0) {
|
||||||
this.vendingItems = new TIntArrayList();
|
this.vendingItems = new TIntArrayList();
|
||||||
String[] vendingIds = set.getString("vending_ids").replace(";", ",").split(",");
|
for (int vendingId : vendingIds) {
|
||||||
for (String s : vendingIds) {
|
this.vendingItems.add(vendingId);
|
||||||
this.vendingItems.add(Integer.parseInt(s.replace(" ", "")));
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.vendingItems = new TIntArrayList();
|
||||||
}
|
}
|
||||||
|
|
||||||
//if(this.interactionType.getType() == InteractionMultiHeight.class || this.interactionType.getType().isAssignableFrom(InteractionMultiHeight.class))
|
//if(this.interactionType.getType() == InteractionMultiHeight.class || this.interactionType.getType().isAssignableFrom(InteractionMultiHeight.class))
|
||||||
{
|
{
|
||||||
if (set.getString("multiheight").contains(";")) {
|
this.multiHeights = ItemDataGuard.parseHeights(set.getString("multiheight"));
|
||||||
String[] s = set.getString("multiheight").split(";");
|
|
||||||
this.multiHeights = new double[s.length];
|
|
||||||
|
|
||||||
for (int i = 0; i < s.length; i++) {
|
|
||||||
this.multiHeights[i] = Double.parseDouble(s[i]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.multiHeights = new double[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rotations = 4;
|
this.rotations = 4;
|
||||||
@@ -167,6 +160,20 @@ public class Item implements ISerialize {
|
|||||||
return this.fullName;
|
return this.fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display name for user-facing/log output, sourced from furnidata (by classname).
|
||||||
|
* Falls back to the DB public_name when furnidata has no entry or names are disabled.
|
||||||
|
* Never returns null.
|
||||||
|
*/
|
||||||
|
public String getDisplayName() {
|
||||||
|
FurnitureTextProvider provider = (Emulator.getGameEnvironment() != null)
|
||||||
|
? Emulator.getGameEnvironment().getFurnitureTextProvider()
|
||||||
|
: null;
|
||||||
|
String name = (provider != null) ? provider.getName(this.name) : null;
|
||||||
|
if (name != null && !name.isBlank()) return name;
|
||||||
|
return (this.fullName != null) ? this.fullName : "";
|
||||||
|
}
|
||||||
|
|
||||||
public FurnitureType getType() {
|
public FurnitureType getType() {
|
||||||
return this.type;
|
return this.type;
|
||||||
}
|
}
|
||||||
@@ -240,6 +247,10 @@ public class Item implements ISerialize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getRandomVendingItem() {
|
public int getRandomVendingItem() {
|
||||||
|
if (this.vendingItems == null || this.vendingItems.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
return this.vendingItems.get(Emulator.getRandom().nextInt(this.vendingItems.size()));
|
return this.vendingItems.get(Emulator.getRandom().nextInt(this.vendingItems.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,21 +270,23 @@ public class Item implements ISerialize {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serialize(ServerMessage message) {
|
public void serialize(ServerMessage message) {
|
||||||
message.appendString(this.type.code.toLowerCase());
|
message.appendString(this.type == null ? "" : this.type.code.toLowerCase());
|
||||||
|
|
||||||
if (type == FurnitureType.BADGE) {
|
if (type == FurnitureType.BADGE) {
|
||||||
message.appendString(this.customParams);
|
message.appendString(ItemDataGuard.safeString(this.customParams));
|
||||||
} else {
|
} else {
|
||||||
message.appendInt(this.spriteId);
|
message.appendInt(this.spriteId);
|
||||||
|
|
||||||
if (this.getName().contains("wallpaper_single") || this.getName().contains("floor_single") || this.getName().contains("landscape_single")) {
|
String itemName = ItemDataGuard.safeString(this.getName());
|
||||||
message.appendString(this.name.split("_")[2]);
|
if (itemName.contains("wallpaper_single") || itemName.contains("floor_single") || itemName.contains("landscape_single")) {
|
||||||
|
String[] nameParts = itemName.split("_");
|
||||||
|
message.appendString(nameParts.length > 2 ? nameParts[2] : "");
|
||||||
} else if (type == FurnitureType.ROBOT) {
|
} else if (type == FurnitureType.ROBOT) {
|
||||||
message.appendString(this.customParams);
|
message.appendString(ItemDataGuard.safeString(this.customParams));
|
||||||
} else if (name.equalsIgnoreCase("poster")) {
|
} else if (itemName.equalsIgnoreCase("poster")) {
|
||||||
message.appendString(this.customParams);
|
message.appendString(ItemDataGuard.safeString(this.customParams));
|
||||||
} else if (name.startsWith("SONG ")) {
|
} else if (itemName.startsWith("SONG ")) {
|
||||||
message.appendString(this.customParams);
|
message.appendString(ItemDataGuard.safeString(this.customParams));
|
||||||
} else {
|
} else {
|
||||||
message.appendString("");
|
message.appendString("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
final class ItemDataGuard {
|
||||||
|
static final int MAX_EXTRA_DATA_LENGTH = 1000;
|
||||||
|
|
||||||
|
private ItemDataGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String safeString(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String normalizeExtraData(String value) {
|
||||||
|
String safe = safeString(value);
|
||||||
|
return safe.length() > MAX_EXTRA_DATA_LENGTH ? safe.substring(0, MAX_EXTRA_DATA_LENGTH) : safe;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parsePositiveInt(String value) {
|
||||||
|
try {
|
||||||
|
int parsed = Integer.parseInt(safeString(value).trim());
|
||||||
|
return parsed > 0 ? parsed : 0;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int[] parsePositiveIntList(String value) {
|
||||||
|
String safe = safeString(value).replace(";", ",").replace(".", ",");
|
||||||
|
if (safe.isBlank()) {
|
||||||
|
return new int[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = safe.split(",");
|
||||||
|
int[] parsed = new int[parts.length];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (String part : parts) {
|
||||||
|
int id = parsePositiveInt(part);
|
||||||
|
if (id > 0) {
|
||||||
|
parsed[count++] = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == parsed.length) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] compact = new int[count];
|
||||||
|
System.arraycopy(parsed, 0, compact, 0, count);
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double[] parseHeights(String value) {
|
||||||
|
String safe = safeString(value);
|
||||||
|
if (safe.isBlank() || !safe.contains(";")) {
|
||||||
|
return new double[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = safe.split(";");
|
||||||
|
double[] parsed = new double[parts.length];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (String part : parts) {
|
||||||
|
try {
|
||||||
|
double height = Double.parseDouble(part.trim());
|
||||||
|
if (Double.isFinite(height)) {
|
||||||
|
parsed[count++] = height;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignore malformed DB values and keep the remaining heights usable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == parsed.length) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] compact = new double[count];
|
||||||
|
System.arraycopy(parsed, 0, compact, 0, count);
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -566,6 +566,10 @@ public class ItemManager {
|
|||||||
|
|
||||||
|
|
||||||
public int calculateCrackState(int count, int max, Item baseItem) {
|
public int calculateCrackState(int count, int max, Item baseItem) {
|
||||||
|
if (count <= 0 || max <= 0 || baseItem == null || baseItem.getStateCount() <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
return (int) Math.floor((1.0D / ((double) max / (double) count) * baseItem.getStateCount()));
|
return (int) Math.floor((1.0D / ((double) max / (double) count) * baseItem.getStateCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,7 +578,8 @@ public class ItemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Item getCrackableReward(int itemId) {
|
public Item getCrackableReward(int itemId) {
|
||||||
return this.getItem(this.crackableRewards.get(itemId).getRandomReward());
|
CrackableReward reward = this.crackableRewards.get(itemId);
|
||||||
|
return reward == null ? null : this.getItem(reward.getRandomReward());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -604,6 +609,12 @@ public class ItemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HabboItem createItem(int habboId, Item item, int limitedStack, int limitedSells, String extraData) {
|
public HabboItem createItem(int habboId, Item item, int limitedStack, int limitedSells, String extraData) {
|
||||||
|
if (habboId <= 0 || item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
extraData = ItemDataGuard.normalizeExtraData(extraData);
|
||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data, limited_data) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data, limited_data) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
|
||||||
statement.setInt(1, habboId);
|
statement.setInt(1, habboId);
|
||||||
statement.setInt(2, item.getId());
|
statement.setInt(2, item.getId());
|
||||||
@@ -673,6 +684,12 @@ public class ItemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HabboItem handleRecycle(Habbo habbo, String itemId) {
|
public HabboItem handleRecycle(Habbo habbo, String itemId) {
|
||||||
|
int rewardItemId = ItemDataGuard.parsePositiveInt(itemId);
|
||||||
|
if (habbo == null || habbo.getHabboInfo() == null || rewardItemId <= 0
|
||||||
|
|| Emulator.getGameEnvironment().getCatalogManager().ecotronItem == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String extradata = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR);
|
String extradata = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR);
|
||||||
|
|
||||||
HabboItem item = null;
|
HabboItem item = null;
|
||||||
@@ -686,7 +703,7 @@ public class ItemManager {
|
|||||||
try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO items_presents (item_id, base_item_reward) VALUES (?, ?)")) {
|
try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO items_presents (item_id, base_item_reward) VALUES (?, ?)")) {
|
||||||
while (set.next() && item == null) {
|
while (set.next() && item == null) {
|
||||||
preparedStatement.setInt(1, set.getInt(1));
|
preparedStatement.setInt(1, set.getInt(1));
|
||||||
preparedStatement.setInt(2, Integer.parseInt(itemId));
|
preparedStatement.setInt(2, rewardItemId);
|
||||||
preparedStatement.addBatch();
|
preparedStatement.addBatch();
|
||||||
item = new InteractionDefault(set.getInt(1), habbo.getHabboInfo().getId(), Emulator.getGameEnvironment().getCatalogManager().ecotronItem, extradata, 0, 0);
|
item = new InteractionDefault(set.getInt(1), habbo.getHabboInfo().getId(), Emulator.getGameEnvironment().getCatalogManager().ecotronItem, extradata, 0, 0);
|
||||||
}
|
}
|
||||||
@@ -829,6 +846,10 @@ public class ItemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HabboItem createGift(String username, Item item, String extraData, int limitedStack, int limitedSells) {
|
public HabboItem createGift(String username, Item item, String extraData, int limitedStack, int limitedSells) {
|
||||||
|
if (username == null || username.isBlank() || item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
||||||
|
|
||||||
int userId = 0;
|
int userId = 0;
|
||||||
@@ -857,13 +878,13 @@ public class ItemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HabboItem createGift(int userId, Item item, String extraData, int limitedStack, int limitedSells) {
|
public HabboItem createGift(int userId, Item item, String extraData, int limitedStack, int limitedSells) {
|
||||||
if (userId == 0)
|
if (userId <= 0 || item == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (extraData.length() > 1000) {
|
if (extraData != null && extraData.length() > ItemDataGuard.MAX_EXTRA_DATA_LENGTH) {
|
||||||
LOGGER.error("Extradata exceeds maximum length of 1000 characters: {}", extraData);
|
LOGGER.error("Extradata exceeds maximum length of 1000 characters: {}", extraData);
|
||||||
extraData = extraData.substring(0, 1000);
|
|
||||||
}
|
}
|
||||||
|
extraData = ItemDataGuard.normalizeExtraData(extraData);
|
||||||
|
|
||||||
HabboItem gift = this.createItem(userId, item, limitedStack, limitedSells, extraData);
|
HabboItem gift = this.createItem(userId, item, limitedStack, limitedSells, extraData);
|
||||||
|
|
||||||
@@ -879,7 +900,7 @@ public class ItemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Item getItem(int itemId) {
|
public Item getItem(int itemId) {
|
||||||
if (itemId < 0)
|
if (itemId <= 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return this.items.get(itemId);
|
return this.items.get(itemId);
|
||||||
@@ -890,12 +911,16 @@ public class ItemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Item getItem(String itemName) {
|
public Item getItem(String itemName) {
|
||||||
|
if (itemName == null || itemName.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
TIntObjectIterator<Item> item = this.items.iterator();
|
TIntObjectIterator<Item> item = this.items.iterator();
|
||||||
|
|
||||||
for (int i = this.items.size(); i-- > 0; ) {
|
for (int i = this.items.size(); i-- > 0; ) {
|
||||||
try {
|
try {
|
||||||
item.advance();
|
item.advance();
|
||||||
if (item.value().getName().equalsIgnoreCase(itemName)) {
|
if (item.value() != null && item.value().getName() != null && item.value().getName().equalsIgnoreCase(itemName)) {
|
||||||
return item.value();
|
return item.value();
|
||||||
}
|
}
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
|
|||||||
+11
@@ -13,11 +13,13 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class InteractionGift extends HabboItem {
|
public class InteractionGift extends HabboItem {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
|
||||||
|
|
||||||
public boolean explode = false;
|
public boolean explode = false;
|
||||||
|
private final AtomicBoolean opening = new AtomicBoolean(false);
|
||||||
private int[] itemId;
|
private int[] itemId;
|
||||||
private int colorId = 0;
|
private int colorId = 0;
|
||||||
private int ribbonId = 0;
|
private int ribbonId = 0;
|
||||||
@@ -46,6 +48,15 @@ public class InteractionGift extends HabboItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims the right to open this gift, returning true exactly once. Guards
|
||||||
|
* against two near-simultaneous OpenRecycleBox packets both scheduling an
|
||||||
|
* (async, delayed) OpenGift before the wrapper is removed from the room.
|
||||||
|
*/
|
||||||
|
public boolean tryStartOpening() {
|
||||||
|
return this.opening.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serializeExtradata(ServerMessage serverMessage) {
|
public void serializeExtradata(ServerMessage serverMessage) {
|
||||||
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
|
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
|
||||||
|
|||||||
+2
-3
@@ -65,9 +65,8 @@ public class InteractionMultiHeight extends HabboItem {
|
|||||||
if (this.getBaseItem().getMultiHeights().length > 0) {
|
if (this.getBaseItem().getMultiHeights().length > 0) {
|
||||||
this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length));
|
this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length));
|
||||||
this.needsUpdate(true);
|
this.needsUpdate(true);
|
||||||
room.updateTiles(room.getLayout().getTilesAt(room.getLayout().getTile(this.getX(), this.getY()), this.getBaseItem().getWidth(), this.getBaseItem().getLength(), this.getRotation()));
|
room.updateItem(this);
|
||||||
room.updateItemState(this);
|
this.updateUnitsOnItem(room);
|
||||||
//room.sendComposer(new UpdateStackHeightComposer(this.getX(), this.getY(), this.getBaseItem().getMultiHeights()[Integer.valueOf(this.getExtradata())] * 256.0D).compose());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.items.interactions;
|
|||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
import com.eu.habbo.habbohotel.items.Item;
|
import com.eu.habbo.habbohotel.items.Item;
|
||||||
|
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||||
import com.eu.habbo.habbohotel.rooms.Room;
|
import com.eu.habbo.habbohotel.rooms.Room;
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomLayout;
|
import com.eu.habbo.habbohotel.rooms.RoomLayout;
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomUnit;
|
import com.eu.habbo.habbohotel.rooms.RoomUnit;
|
||||||
@@ -133,12 +134,18 @@ public class InteractionRentableSpace extends HabboItem {
|
|||||||
if (habbo.getHabboStats().isRentingSpace())
|
if (habbo.getHabboStats().isRentingSpace())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (habbo.getHabboInfo().getCredits() < this.rentCost())
|
int cost = this.rentCost();
|
||||||
|
boolean hasInfiniteCredits = habbo.hasPermission(Permission.ACC_INFINITE_CREDITS);
|
||||||
|
if (!hasInfiniteCredits && habbo.getHabboInfo().getCredits() < cost)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (habbo.getHabboStats().getClubExpireTimestamp() < Emulator.getIntUnixTimestamp())
|
if (habbo.getHabboStats().getClubExpireTimestamp() < Emulator.getIntUnixTimestamp())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!hasInfiniteCredits) {
|
||||||
|
habbo.giveCredits(-cost);
|
||||||
|
}
|
||||||
|
|
||||||
this.setRenterId(habbo.getHabboInfo().getId());
|
this.setRenterId(habbo.getHabboInfo().getId());
|
||||||
this.setRenterName(habbo.getHabboInfo().getUsername());
|
this.setRenterName(habbo.getHabboInfo().getUsername());
|
||||||
this.setEndTimestamp(Emulator.getIntUnixTimestamp() + (7 * 86400));
|
this.setEndTimestamp(Emulator.getIntUnixTimestamp() + (7 * 86400));
|
||||||
|
|||||||
+21
-27
@@ -2,6 +2,7 @@ package com.eu.habbo.habbohotel.items.interactions;
|
|||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.habbohotel.items.Item;
|
import com.eu.habbo.habbohotel.items.Item;
|
||||||
|
import com.eu.habbo.habbohotel.items.interactions.wired.WiredInputGuard;
|
||||||
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
|
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
|
||||||
import com.eu.habbo.habbohotel.rooms.Room;
|
import com.eu.habbo.habbohotel.rooms.Room;
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomUnit;
|
import com.eu.habbo.habbohotel.rooms.RoomUnit;
|
||||||
@@ -18,6 +19,7 @@ import java.sql.SQLException;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
|
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
|
||||||
@@ -61,7 +63,11 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
*/
|
*/
|
||||||
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
|
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
private long cooldown;
|
private volatile long cooldown;
|
||||||
|
// Ensures one box is processed by a single thread at a time, so the
|
||||||
|
// cooldown check-and-set in WiredHandler can't double-fire when a packet
|
||||||
|
// thread and the room cycle thread trigger the same box concurrently.
|
||||||
|
private final AtomicBoolean processing = new AtomicBoolean(false);
|
||||||
private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
|
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
|
||||||
@@ -149,6 +155,15 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
this.cooldown = newMillis;
|
this.cooldown = newMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Claims exclusive processing of this box; returns false if another thread is already in it. */
|
||||||
|
public boolean tryBeginProcessing() {
|
||||||
|
return this.processing.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endProcessing() {
|
||||||
|
this.processing.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean allowWiredResetState() {
|
public boolean allowWiredResetState() {
|
||||||
return false;
|
return false;
|
||||||
@@ -216,39 +231,18 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
|
|
||||||
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
|
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
|
||||||
{
|
{
|
||||||
int intParamCount = packet.readInt();
|
int[] intParams = WiredInputGuard.readIntParams(packet);
|
||||||
if (intParamCount < 0 || intParamCount > 100) {
|
String stringParam = WiredInputGuard.readStringParam(packet);
|
||||||
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount);
|
int[] itemIds = WiredInputGuard.readFurniIds(packet);
|
||||||
}
|
|
||||||
int[] intParams = new int[intParamCount];
|
|
||||||
|
|
||||||
for(int i = 0; i < intParamCount; i++)
|
|
||||||
{
|
|
||||||
intParams[i] = packet.readInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
String stringParam = packet.readString();
|
|
||||||
|
|
||||||
int itemCount = packet.readInt();
|
|
||||||
int selectionLimit = Emulator.getConfig() != null ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) : 5;
|
|
||||||
if (itemCount < 0 || itemCount > selectionLimit * 20) {
|
|
||||||
throw new IllegalArgumentException("Invalid itemCount: " + itemCount + " exceeds maximum allowed limit");
|
|
||||||
}
|
|
||||||
int[] itemIds = new int[itemCount];
|
|
||||||
|
|
||||||
for(int i = 0; i < itemCount; i++)
|
|
||||||
{
|
|
||||||
itemIds[i] = packet.readInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
WiredSettings settings = new WiredSettings(intParams, stringParam, itemIds, -1);
|
WiredSettings settings = new WiredSettings(intParams, stringParam, itemIds, -1);
|
||||||
|
|
||||||
if(isEffect)
|
if(isEffect)
|
||||||
{
|
{
|
||||||
settings.setDelay(packet.readInt());
|
settings.setDelay(WiredInputGuard.normalizeDelay(packet.readInt()));
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.setStuffTypeSelectionCode(packet.readInt());
|
settings.setStuffTypeSelectionCode(WiredInputGuard.normalizeStuffSelectionCode(packet.readInt()));
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -190,6 +190,14 @@ public class InteractionPetBreedingNest extends HabboItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
|
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
|
||||||
|
// Guard before the destructive delete below: a crafted packet can call
|
||||||
|
// this on a nest that isn't full, which would delete the nest furni and
|
||||||
|
// then NPE on petOne/petTwo in the async runnable (losing the furni).
|
||||||
|
if (habbo == null || this.petOne == null || this.petTwo == null
|
||||||
|
|| habbo.getHabboInfo().getCurrentRoom() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
|
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
|
||||||
|
|
||||||
this.setExtradata("2");
|
this.setExtradata("2");
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items.interactions.wired;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.messages.ClientMessage;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class WiredInputGuard {
|
||||||
|
public static final int MAX_INT_PARAMS = 100;
|
||||||
|
public static final int MAX_STRING_PARAM_LENGTH = 1024;
|
||||||
|
public static final int MAX_ABSOLUTE_FURNI_IDS = 100;
|
||||||
|
public static final int DEFAULT_MAX_DELAY = 20;
|
||||||
|
public static final int MAX_ABSOLUTE_DELAY = 3600;
|
||||||
|
public static final int MIN_STUFF_SELECTION_CODE = -1;
|
||||||
|
public static final int MAX_STUFF_SELECTION_CODE = 2;
|
||||||
|
|
||||||
|
private WiredInputGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] readIntParams(ClientMessage packet) {
|
||||||
|
int count = packet.readInt();
|
||||||
|
if (count < 0 || count > MAX_INT_PARAMS) {
|
||||||
|
throw new IllegalArgumentException("Invalid wired int param count");
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] values = new int[count];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
values[i] = packet.readInt();
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readStringParam(ClientMessage packet) {
|
||||||
|
String value = packet.readString();
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.length() > MAX_STRING_PARAM_LENGTH
|
||||||
|
? value.substring(0, MAX_STRING_PARAM_LENGTH)
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] readFurniIds(ClientMessage packet) {
|
||||||
|
int count = packet.readInt();
|
||||||
|
int maxCount = maxFurniSelectionCount();
|
||||||
|
if (count < 0 || count > maxCount) {
|
||||||
|
throw new IllegalArgumentException("Invalid wired furni selection count");
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] values = new int[count];
|
||||||
|
int accepted = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int itemId = packet.readInt();
|
||||||
|
if (itemId > 0) {
|
||||||
|
values[accepted++] = itemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accepted == values.length ? values : Arrays.copyOf(values, accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int normalizeDelay(int delay) {
|
||||||
|
return Math.max(0, Math.min(delay, maxDelay()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int normalizeStuffSelectionCode(int code) {
|
||||||
|
if (code < MIN_STUFF_SELECTION_CODE || code > MAX_STUFF_SELECTION_CODE) {
|
||||||
|
return MIN_STUFF_SELECTION_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int maxFurniSelectionCount() {
|
||||||
|
int selectionLimit = Emulator.getConfig() != null
|
||||||
|
? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5)
|
||||||
|
: 5;
|
||||||
|
selectionLimit = Math.max(1, selectionLimit);
|
||||||
|
return Math.min(MAX_ABSOLUTE_FURNI_IDS, selectionLimit * 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int maxDelay() {
|
||||||
|
int configured = Emulator.getConfig() != null
|
||||||
|
? Emulator.getConfig().getInt("hotel.wired.max_delay", DEFAULT_MAX_DELAY)
|
||||||
|
: DEFAULT_MAX_DELAY;
|
||||||
|
configured = Math.max(0, configured);
|
||||||
|
return Math.min(MAX_ABSOLUTE_DELAY, configured);
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items.interactions.wired;
|
||||||
|
|
||||||
|
import com.eu.habbo.habbohotel.rooms.Room;
|
||||||
|
import com.eu.habbo.habbohotel.users.HabboItem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class WiredLegacyDataGuard {
|
||||||
|
public static final int DEFAULT_MAX_DELAY = 20;
|
||||||
|
|
||||||
|
private WiredLegacyDataGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int parseDelay(String value) {
|
||||||
|
try {
|
||||||
|
int parsed = Integer.parseInt(value == null ? "" : value.trim());
|
||||||
|
return Math.max(0, Math.min(parsed, DEFAULT_MAX_DELAY));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<HabboItem> parseRoomItems(String value, Room room) {
|
||||||
|
List<HabboItem> items = new ArrayList<>();
|
||||||
|
if (room == null || value == null || value.isBlank()) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String part : value.split(";")) {
|
||||||
|
try {
|
||||||
|
int itemId = Integer.parseInt(part.trim());
|
||||||
|
if (itemId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
HabboItem item = room.getHabboItem(itemId);
|
||||||
|
if (item != null) {
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignore malformed legacy ids and keep loading the remaining items.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items.interactions.wired;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
|
||||||
|
public final class WiredNumericInputGuard {
|
||||||
|
public static final int DEFAULT_MAX_REWARD_AMOUNT = 1000;
|
||||||
|
public static final int DEFAULT_MAX_RESPECT_AMOUNT = 100;
|
||||||
|
public static final int MAX_ABSOLUTE_AMOUNT = 100000;
|
||||||
|
|
||||||
|
private WiredNumericInputGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int parsePositiveAmount(String value, int maxAmount) {
|
||||||
|
try {
|
||||||
|
int parsed = Integer.parseInt(value == null ? "" : value.trim());
|
||||||
|
if (parsed <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(parsed, Math.max(1, Math.min(maxAmount, MAX_ABSOLUTE_AMOUNT)));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int maxRewardAmount() {
|
||||||
|
return configuredMax("hotel.wired.reward.max_amount", DEFAULT_MAX_REWARD_AMOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int maxRespectAmount() {
|
||||||
|
return configuredMax("hotel.wired.respect.max_amount", DEFAULT_MAX_RESPECT_AMOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int configuredMax(String key, int fallback) {
|
||||||
|
int configured = Emulator.getConfig() != null ? Emulator.getConfig().getInt(key, fallback) : fallback;
|
||||||
|
return Math.max(1, Math.min(configured, MAX_ABSOLUTE_AMOUNT));
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items.interactions.wired;
|
||||||
|
|
||||||
|
public final class WiredTimerInputGuard {
|
||||||
|
public static final int MAX_TIMER_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
private WiredTimerInputGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int fromClientUnits(int units, int stepMs, int minMs) {
|
||||||
|
return fromClientUnits(units, stepMs, minMs, MAX_TIMER_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int fromClientUnits(int units, int stepMs, int minMs, int maxMs) {
|
||||||
|
if (units < 1 || stepMs < 1) {
|
||||||
|
return minMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
long value = (long) units * stepMs;
|
||||||
|
return clamp(value, minMs, maxMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int normalizeStoredMillis(Integer millis, int minMs, int fallbackMs) {
|
||||||
|
return normalizeStoredMillis(millis, minMs, fallbackMs, MAX_TIMER_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int normalizeStoredMillis(Integer millis, int minMs, int fallbackMs, int maxMs) {
|
||||||
|
if (millis == null || millis < minMs) {
|
||||||
|
return fallbackMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clamp(millis.longValue(), minMs, maxMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(long value, int minMs, int maxMs) {
|
||||||
|
if (value < minMs) {
|
||||||
|
return minMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > maxMs) {
|
||||||
|
return maxMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-4
@@ -111,7 +111,13 @@ public class WiredConditionActorDir extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
@@ -157,15 +163,15 @@ public class WiredConditionActorDir extends InteractionWiredCondition {
|
|||||||
return (this.directionMask & (1 << direction)) != 0;
|
return (this.directionMask & (1 << direction)) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeDirectionMask(int value) {
|
int normalizeDirectionMask(int value) {
|
||||||
return value & ALL_DIRECTIONS_MASK;
|
return value & ALL_DIRECTIONS_MASK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeUserSource(int value) {
|
int normalizeUserSource(int value) {
|
||||||
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeQuantifier(int value) {
|
int normalizeQuantifier(int value) {
|
||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-13
@@ -111,19 +111,21 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
this.items.clear();
|
this.resetSettings();
|
||||||
this.comparison = COMPARISON_EQUAL;
|
|
||||||
this.minutes = 0;
|
|
||||||
this.halfSecondSteps = 0;
|
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
|
|
||||||
this.quantifier = QUANTIFIER_ALL;
|
|
||||||
|
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) {
|
if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.resetSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -131,7 +133,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
|
|||||||
this.comparison = this.normalizeComparison(data.comparison);
|
this.comparison = this.normalizeComparison(data.comparison);
|
||||||
this.minutes = this.normalizeMinutes(data.minutes);
|
this.minutes = this.normalizeMinutes(data.minutes);
|
||||||
this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps);
|
this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps);
|
||||||
this.furniSource = data.furniSource;
|
this.furniSource = this.normalizeFurniSource(data.furniSource);
|
||||||
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
||||||
|
|
||||||
if (data.itemIds == null) {
|
if (data.itemIds == null) {
|
||||||
@@ -139,6 +141,10 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Integer id : data.itemIds) {
|
for (Integer id : data.itemIds) {
|
||||||
|
if (id == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
HabboItem item = room.getHabboItem(id);
|
HabboItem item = room.getHabboItem(id);
|
||||||
if (item instanceof InteractionGameUpCounter) {
|
if (item instanceof InteractionGameUpCounter) {
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
@@ -195,7 +201,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
|
|||||||
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
|
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
|
||||||
this.minutes = (params.length > 1) ? this.normalizeMinutes(params[1]) : 0;
|
this.minutes = (params.length > 1) ? this.normalizeMinutes(params[1]) : 0;
|
||||||
this.halfSecondSteps = (params.length > 2) ? this.normalizeHalfSecondSteps(params[2]) : 0;
|
this.halfSecondSteps = (params.length > 2) ? this.normalizeHalfSecondSteps(params[2]) : 0;
|
||||||
this.furniSource = (params.length > 3) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.furniSource = (params.length > 3) ? this.normalizeFurniSource(params[3]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = (params.length > 4) ? this.normalizeQuantifier(params[4]) : QUANTIFIER_ALL;
|
this.quantifier = (params.length > 4) ? this.normalizeQuantifier(params[4]) : QUANTIFIER_ALL;
|
||||||
|
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
@@ -224,6 +230,15 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void resetSettings() {
|
||||||
|
this.items.clear();
|
||||||
|
this.comparison = COMPARISON_EQUAL;
|
||||||
|
this.minutes = 0;
|
||||||
|
this.halfSecondSteps = 0;
|
||||||
|
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
this.quantifier = QUANTIFIER_ALL;
|
||||||
|
}
|
||||||
|
|
||||||
private void refresh(Room room) {
|
private void refresh(Room room) {
|
||||||
THashSet<HabboItem> remove = new THashSet<>();
|
THashSet<HabboItem> remove = new THashSet<>();
|
||||||
|
|
||||||
@@ -256,7 +271,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeComparison(int value) {
|
int normalizeComparison(int value) {
|
||||||
if (value < COMPARISON_LESS || value > COMPARISON_GREATER) {
|
if (value < COMPARISON_LESS || value > COMPARISON_GREATER) {
|
||||||
return COMPARISON_EQUAL;
|
return COMPARISON_EQUAL;
|
||||||
}
|
}
|
||||||
@@ -264,18 +279,30 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeMinutes(int value) {
|
int normalizeMinutes(int value) {
|
||||||
return Math.max(0, Math.min(MAX_MINUTES, value));
|
return Math.max(0, Math.min(MAX_MINUTES, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeHalfSecondSteps(int value) {
|
int normalizeHalfSecondSteps(int value) {
|
||||||
return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value));
|
return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeQuantifier(int value) {
|
int normalizeQuantifier(int value) {
|
||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int normalizeFurniSource(int value) {
|
||||||
|
switch (value) {
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTED:
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTOR:
|
||||||
|
case WiredSourceUtil.SOURCE_SIGNAL:
|
||||||
|
case WiredSourceUtil.SOURCE_TRIGGER:
|
||||||
|
return value;
|
||||||
|
default:
|
||||||
|
return WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int comparison;
|
int comparison;
|
||||||
int minutes;
|
int minutes;
|
||||||
|
|||||||
+19
-6
@@ -53,8 +53,7 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition {
|
|||||||
@Override
|
@Override
|
||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 2) return false;
|
if(settings.getIntParams().length < 2) return false;
|
||||||
this.startDate = settings.getIntParams()[0];
|
this.applyRange(settings.getIntParams()[0], settings.getIntParams()[1]);
|
||||||
this.endDate = settings.getIntParams()[1];
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,20 +79,28 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
|
this.onPickUp();
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
this.applyRange(0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.startDate = data.startDate;
|
if (data == null) {
|
||||||
this.endDate = data.endDate;
|
this.applyRange(0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.applyRange(data.startDate, data.endDate);
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split("\t");
|
String[] data = wiredData.split("\t");
|
||||||
|
|
||||||
if (data.length == 2) {
|
if (data.length == 2) {
|
||||||
try {
|
try {
|
||||||
this.startDate = Integer.parseInt(data[0]);
|
this.applyRange(Integer.parseInt(data[0]), Integer.parseInt(data[1]));
|
||||||
this.endDate = Integer.parseInt(data[1]);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
this.applyRange(0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +112,12 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition {
|
|||||||
this.endDate = 0;
|
this.endDate = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyRange(int startDate, int endDate) {
|
||||||
|
int[] range = WiredDateRangeInputGuard.normalizeRange(startDate, endDate);
|
||||||
|
this.startDate = range[0];
|
||||||
|
this.endDate = range[1];
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int startDate;
|
int startDate;
|
||||||
int endDate;
|
int endDate;
|
||||||
|
|||||||
+38
-17
@@ -92,21 +92,35 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
|
this.onPickUp();
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
this.all = data.all;
|
try {
|
||||||
this.furniSource = data.furniSource;
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for(int id : data.itemIds) {
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.all = data.all;
|
||||||
|
this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
|
||||||
|
|
||||||
|
for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
HabboItem item = room.getHabboItem(id);
|
HabboItem item = room.getHabboItem(id);
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
@@ -114,21 +128,18 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
this.all = (data[0].equals("1"));
|
this.all = (data[0].equals("1"));
|
||||||
|
|
||||||
if (data.length == 2) {
|
if (data.length == 2) {
|
||||||
String[] items = data[1].split(";");
|
for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
|
HabboItem item = room.getHabboItem(id);
|
||||||
|
|
||||||
for (String s : items) {
|
if (item != null) {
|
||||||
HabboItem item = room.getHabboItem(Integer.parseInt(s));
|
|
||||||
|
|
||||||
if (item != null)
|
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||||
}
|
}
|
||||||
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -172,14 +183,12 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
|
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.all = params[0] == 1;
|
this.all = params[0] == 1;
|
||||||
this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
|
||||||
int count = settings.getFurniIds().length;
|
int count = settings.getFurniIds().length;
|
||||||
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
|
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
|
||||||
|
|
||||||
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
|
|
||||||
@@ -198,6 +207,18 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int normalizeFurniSource(int value) {
|
||||||
|
switch (value) {
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTED:
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTOR:
|
||||||
|
case WiredSourceUtil.SOURCE_SIGNAL:
|
||||||
|
case WiredSourceUtil.SOURCE_TRIGGER:
|
||||||
|
return value;
|
||||||
|
default:
|
||||||
|
return WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void refresh() {
|
private void refresh() {
|
||||||
THashSet<HabboItem> items = new THashSet<>();
|
THashSet<HabboItem> items = new THashSet<>();
|
||||||
|
|
||||||
|
|||||||
+26
-17
@@ -89,15 +89,18 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
this.items.clear();
|
this.onPickUp();
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.furniSource = data.furniSource;
|
this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
|
||||||
this.all = data.all;
|
this.all = data.all;
|
||||||
|
|
||||||
for(int id : data.itemIds) {
|
for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
HabboItem item = room.getHabboItem(id);
|
HabboItem item = room.getHabboItem(id);
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
@@ -107,23 +110,19 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
if (data.length >= 1) {
|
if (data.length >= 2) {
|
||||||
|
for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
|
HabboItem item = room.getHabboItem(id);
|
||||||
|
|
||||||
String[] items = data[1].split(";");
|
if (item != null) {
|
||||||
|
|
||||||
for (String s : items) {
|
|
||||||
HabboItem item = room.getHabboItem(Integer.parseInt(s));
|
|
||||||
|
|
||||||
if (item != null)
|
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||||
this.all = false;
|
this.all = false;
|
||||||
}
|
}
|
||||||
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -162,11 +161,9 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
|
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.all = (params.length > 0) && (params[0] == 1);
|
this.all = (params.length > 0) && (params[0] == 1);
|
||||||
this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER);
|
this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : ((params.length > 0 && params[0] > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER);
|
||||||
|
|
||||||
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
|
|
||||||
@@ -186,6 +183,18 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int normalizeFurniSource(int value) {
|
||||||
|
switch (value) {
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTED:
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTOR:
|
||||||
|
case WiredSourceUtil.SOURCE_SIGNAL:
|
||||||
|
case WiredSourceUtil.SOURCE_TRIGGER:
|
||||||
|
return value;
|
||||||
|
default:
|
||||||
|
return WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean hasAvatarOnItem(HabboItem item, Room room, Collection<Habbo> habbos, Collection<Bot> bots, Collection<Pet> pets) {
|
protected boolean hasAvatarOnItem(HabboItem item, Room room, Collection<Habbo> habbos, Collection<Bot> bots, Collection<Pet> pets) {
|
||||||
RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY());
|
RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY());
|
||||||
if (baseTile == null) return false;
|
if (baseTile == null) return false;
|
||||||
|
|||||||
+15
-4
@@ -52,6 +52,10 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.quantifier == QUANTIFIER_ANY) {
|
if (this.quantifier == QUANTIFIER_ANY) {
|
||||||
return this.evaluateAnyMatches(ctx);
|
return this.evaluateAnyMatches(ctx);
|
||||||
}
|
}
|
||||||
@@ -158,7 +162,14 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -310,8 +321,8 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadItems(Room room, List<Integer> itemIds, THashSet<HabboItem> target) {
|
void loadItems(Room room, List<Integer> itemIds, THashSet<HabboItem> target) {
|
||||||
if (itemIds == null) {
|
if (room == null || itemIds == null || target == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +346,7 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
|
|||||||
.collect(Collectors.joining(";"));
|
.collect(Collectors.joining(";"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Integer> parseIds(String value) {
|
List<Integer> parseIds(String value) {
|
||||||
List<Integer> result = new ArrayList<>();
|
List<Integer> result = new ArrayList<>();
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+28
-9
@@ -16,6 +16,7 @@ import java.sql.SQLException;
|
|||||||
|
|
||||||
public class WiredConditionHabboCount extends InteractionWiredCondition {
|
public class WiredConditionHabboCount extends InteractionWiredCondition {
|
||||||
public static final WiredConditionType type = WiredConditionType.USER_COUNT;
|
public static final WiredConditionType type = WiredConditionType.USER_COUNT;
|
||||||
|
static final int MAX_USER_COUNT_LIMIT = 1000;
|
||||||
|
|
||||||
private int lowerLimit = 0;
|
private int lowerLimit = 0;
|
||||||
private int upperLimit = 50;
|
private int upperLimit = 50;
|
||||||
@@ -31,6 +32,10 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER)
|
int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER)
|
||||||
? ctx.room().getUserCount()
|
? ctx.room().getUserCount()
|
||||||
: WiredSourceUtil.resolveUsers(ctx, this.userSource).size();
|
: WiredSourceUtil.resolveUsers(ctx, this.userSource).size();
|
||||||
@@ -55,20 +60,29 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
|
this.onPickUp();
|
||||||
|
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.lowerLimit = data.lowerLimit;
|
this.applyLimits(data.lowerLimit, data.upperLimit);
|
||||||
this.upperLimit = data.upperLimit;
|
this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource);
|
||||||
this.userSource = data.userSource;
|
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
this.lowerLimit = Integer.parseInt(data[0]);
|
if (data.length >= 2) {
|
||||||
this.upperLimit = Integer.parseInt(data[1]);
|
try {
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.applyLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim()));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep the constructed defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -104,14 +118,19 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
|
|||||||
@Override
|
@Override
|
||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 2) return false;
|
if(settings.getIntParams().length < 2) return false;
|
||||||
this.lowerLimit = settings.getIntParams()[0];
|
|
||||||
this.upperLimit = settings.getIntParams()[1];
|
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.applyLimits(params[0], params[1]);
|
||||||
|
this.userSource = (params.length > 2) ? WiredConditionInputGuard.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyLimits(int lowerLimit, int upperLimit) {
|
||||||
|
int[] limits = WiredConditionInputGuard.normalizeUserCountRange(lowerLimit, upperLimit);
|
||||||
|
this.lowerLimit = limits[0];
|
||||||
|
this.upperLimit = limits[1];
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int lowerLimit;
|
int lowerLimit;
|
||||||
int upperLimit;
|
int upperLimit;
|
||||||
|
|||||||
+34
-6
@@ -18,6 +18,7 @@ import java.util.List;
|
|||||||
public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
|
public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
|
||||||
protected static final int QUANTIFIER_ALL = 0;
|
protected static final int QUANTIFIER_ALL = 0;
|
||||||
protected static final int QUANTIFIER_ANY = 1;
|
protected static final int QUANTIFIER_ANY = 1;
|
||||||
|
protected static final int MAX_EFFECT_ID = 10_000;
|
||||||
|
|
||||||
public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_EFFECT;
|
public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_EFFECT;
|
||||||
|
|
||||||
@@ -86,15 +87,34 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
|
this.onPickUp();
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
this.effectId = data.effectId;
|
try {
|
||||||
this.userSource = data.userSource;
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data == null) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.effectId = WiredUserConditionInputGuard.normalizeEffectId(data.effectId);
|
||||||
|
this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource);
|
||||||
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
|
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
|
||||||
} else {
|
} else {
|
||||||
this.effectId = Integer.parseInt(wiredData);
|
try {
|
||||||
|
this.effectId = WiredUserConditionInputGuard.normalizeEffectId(Integer.parseInt(wiredData));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
this.effectId = 0;
|
||||||
|
}
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = QUANTIFIER_ANY;
|
this.quantifier = QUANTIFIER_ANY;
|
||||||
}
|
}
|
||||||
@@ -134,8 +154,8 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
|
|||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 1) return false;
|
if(settings.getIntParams().length < 1) return false;
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.effectId = params[0];
|
this.effectId = WiredUserConditionInputGuard.normalizeEffectId(params[0]);
|
||||||
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = (params.length > 1) ? WiredUserConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ANY) : QUANTIFIER_ANY;
|
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ANY) : QUANTIFIER_ANY;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -153,6 +173,14 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition {
|
|||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected int normalizeEffectId(int value) {
|
||||||
|
return Math.max(0, Math.min(MAX_EFFECT_ID, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int normalizeUserSource(int value) {
|
||||||
|
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int effectId;
|
int effectId;
|
||||||
int userSource;
|
int userSource;
|
||||||
|
|||||||
+34
-22
@@ -10,17 +10,15 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
|
|||||||
import com.eu.habbo.habbohotel.wired.core.WiredContext;
|
import com.eu.habbo.habbohotel.wired.core.WiredContext;
|
||||||
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
|
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
|
||||||
import com.eu.habbo.messages.ServerMessage;
|
import com.eu.habbo.messages.ServerMessage;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
|
public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredConditionHabboHasHandItem.class);
|
|
||||||
protected static final int QUANTIFIER_ALL = 0;
|
protected static final int QUANTIFIER_ALL = 0;
|
||||||
protected static final int QUANTIFIER_ANY = 1;
|
protected static final int QUANTIFIER_ANY = 1;
|
||||||
|
protected static final int MAX_HAND_ITEM_ID = 10_000;
|
||||||
|
|
||||||
public static final WiredConditionType type = WiredConditionType.ACTOR_HAS_HANDITEM;
|
public static final WiredConditionType type = WiredConditionType.ACTOR_HAS_HANDITEM;
|
||||||
|
|
||||||
@@ -62,9 +60,9 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
|
|||||||
@Override
|
@Override
|
||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 1) return false;
|
if(settings.getIntParams().length < 1) return false;
|
||||||
this.handItem = this.normalizeHandItem(settings.getIntParams()[0]);
|
this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(settings.getIntParams()[0]);
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = (params.length > 1) ? WiredUserConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
|
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -99,21 +97,35 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
try {
|
String wiredData = set.getString("wired_data");
|
||||||
String wiredData = set.getString("wired_data");
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
this.handItem = this.normalizeHandItem(data.handItemId);
|
try {
|
||||||
this.userSource = data.userSource;
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
} catch (RuntimeException ignored) {
|
||||||
} else {
|
this.onPickUp();
|
||||||
this.handItem = this.normalizeHandItem(Integer.parseInt(wiredData));
|
return;
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
|
||||||
this.quantifier = QUANTIFIER_ALL;
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
if (data == null) {
|
||||||
LOGGER.error("Caught exception", e);
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(data.handItemId);
|
||||||
|
this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource);
|
||||||
|
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(Integer.parseInt(wiredData));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
this.handItem = 0;
|
||||||
|
}
|
||||||
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
this.quantifier = QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,14 +168,14 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int normalizeHandItem(int value) {
|
|
||||||
return Math.max(0, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int normalizeQuantifier(int value) {
|
protected int normalizeQuantifier(int value) {
|
||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected int normalizeUserSource(int value) {
|
||||||
|
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int handItemId;
|
int handItemId;
|
||||||
int userSource;
|
int userSource;
|
||||||
|
|||||||
+39
-6
@@ -20,6 +20,7 @@ import java.util.List;
|
|||||||
public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
|
public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
|
||||||
protected static final int QUANTIFIER_ALL = 0;
|
protected static final int QUANTIFIER_ALL = 0;
|
||||||
protected static final int QUANTIFIER_ANY = 1;
|
protected static final int QUANTIFIER_ANY = 1;
|
||||||
|
protected static final int MAX_BADGE_CODE_LENGTH = 64;
|
||||||
|
|
||||||
public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_BADGE;
|
public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_BADGE;
|
||||||
|
|
||||||
@@ -37,6 +38,10 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Room room = ctx.room();
|
Room room = ctx.room();
|
||||||
List<RoomUnit> targets = WiredSourceUtil.resolveUsers(ctx, this.userSource);
|
List<RoomUnit> targets = WiredSourceUtil.resolveUsers(ctx, this.userSource);
|
||||||
if (targets.isEmpty()) return false;
|
if (targets.isEmpty()) return false;
|
||||||
@@ -102,15 +107,30 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
|
this.onPickUp();
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
this.badge = data.badge;
|
try {
|
||||||
this.userSource = data.userSource;
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data == null) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(data.badge);
|
||||||
|
this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource);
|
||||||
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
|
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
|
||||||
} else {
|
} else {
|
||||||
this.badge = wiredData;
|
this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(wiredData);
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = QUANTIFIER_ANY;
|
this.quantifier = QUANTIFIER_ANY;
|
||||||
}
|
}
|
||||||
@@ -147,9 +167,9 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
this.badge = settings.getStringParam();
|
this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(settings.getStringParam());
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = (params.length > 0) ? WiredUserConditionInputGuard.normalizeUserSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = (params.length > 1) ? this.normalizeQuantifier(params[1], QUANTIFIER_ANY) : QUANTIFIER_ANY;
|
this.quantifier = (params.length > 1) ? this.normalizeQuantifier(params[1], QUANTIFIER_ANY) : QUANTIFIER_ANY;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -167,6 +187,19 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition {
|
|||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String normalizeBadge(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = value.trim();
|
||||||
|
return normalized.length() <= MAX_BADGE_CODE_LENGTH ? normalized : normalized.substring(0, MAX_BADGE_CODE_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int normalizeUserSource(int value) {
|
||||||
|
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
String badge;
|
String badge;
|
||||||
int userSource;
|
int userSource;
|
||||||
|
|||||||
+18
-7
@@ -97,7 +97,14 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,6 +119,10 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Integer id : data.itemIds) {
|
for (Integer id : data.itemIds) {
|
||||||
|
if (id == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
HabboItem item = room.getHabboItem(id);
|
HabboItem item = room.getHabboItem(id);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
@@ -225,7 +236,7 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeComparison(int value) {
|
int normalizeComparison(int value) {
|
||||||
if (value < COMPARISON_LESS || value > COMPARISON_GREATER) {
|
if (value < COMPARISON_LESS || value > COMPARISON_GREATER) {
|
||||||
return COMPARISON_EQUAL;
|
return COMPARISON_EQUAL;
|
||||||
}
|
}
|
||||||
@@ -233,11 +244,11 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeQuantifier(int value) {
|
int normalizeQuantifier(int value) {
|
||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeFurniSource(int value) {
|
int normalizeFurniSource(int value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case WiredSourceUtil.SOURCE_SELECTED:
|
case WiredSourceUtil.SOURCE_SELECTED:
|
||||||
case WiredSourceUtil.SOURCE_SELECTOR:
|
case WiredSourceUtil.SOURCE_SELECTOR:
|
||||||
@@ -249,12 +260,12 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double normalizeAltitude(double value) {
|
double normalizeAltitude(double value) {
|
||||||
double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value));
|
double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value));
|
||||||
return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private double parseAltitudeOrDefault(String value) {
|
double parseAltitudeOrDefault(String value) {
|
||||||
if (value == null || value.trim().isEmpty()) {
|
if (value == null || value.trim().isEmpty()) {
|
||||||
return 0.0D;
|
return 0.0D;
|
||||||
}
|
}
|
||||||
@@ -266,7 +277,7 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatAltitude(double value) {
|
String formatAltitude(double value) {
|
||||||
BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros();
|
BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros();
|
||||||
return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString();
|
return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString();
|
||||||
}
|
}
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
|
||||||
|
|
||||||
|
import com.eu.habbo.habbohotel.games.GameTeamColors;
|
||||||
|
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
|
||||||
|
|
||||||
|
public final class WiredConditionInputGuard {
|
||||||
|
public static final int MAX_USER_COUNT_LIMIT = 1000;
|
||||||
|
public static final int MAX_TIMER_CYCLES = 24 * 60 * 60 * 2;
|
||||||
|
|
||||||
|
private WiredConditionInputGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GameTeamColors normalizeTeamColor(GameTeamColors value, GameTeamColors fallback) {
|
||||||
|
return (value != null) ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GameTeamColors normalizeTeamColorType(int value, GameTeamColors fallback) {
|
||||||
|
for (GameTeamColors color : GameTeamColors.values()) {
|
||||||
|
if (color.type == value) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int normalizeUserSource(int value) {
|
||||||
|
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int normalizeTimerCycles(int value) {
|
||||||
|
if (value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(value, MAX_TIMER_CYCLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] normalizeUserCountRange(int lowerLimit, int upperLimit) {
|
||||||
|
int lower = clampUserCount(lowerLimit);
|
||||||
|
int upper = clampUserCount(upperLimit);
|
||||||
|
|
||||||
|
if (lower > upper) {
|
||||||
|
return new int[]{upper, lower};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new int[]{lower, upper};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clampUserCount(int value) {
|
||||||
|
if (value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(value, MAX_USER_COUNT_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-3
@@ -52,12 +52,13 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition {
|
|||||||
try {
|
try {
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.cycles = data.cycles;
|
this.cycles = WiredConditionInputGuard.normalizeTimerCycles(data.cycles);
|
||||||
} else {
|
} else {
|
||||||
if (!wiredData.equals(""))
|
if (!wiredData.equals(""))
|
||||||
this.cycles = Integer.parseInt(wiredData);
|
this.cycles = WiredConditionInputGuard.normalizeTimerCycles(Integer.parseInt(wiredData));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
this.cycles = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +91,14 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition {
|
|||||||
@Override
|
@Override
|
||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 1) return false;
|
if(settings.getIntParams().length < 1) return false;
|
||||||
this.cycles = settings.getIntParams()[0];
|
this.cycles = WiredConditionInputGuard.normalizeTimerCycles(settings.getIntParams()[0]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int normalizeCycles(int value) {
|
||||||
|
return Math.max(0, Math.min(WiredConditionMoreTimeElapsed.MAX_CYCLES, value));
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int cycles;
|
int cycles;
|
||||||
|
|
||||||
|
|||||||
+13
-6
@@ -125,7 +125,14 @@ public class WiredConditionMatchDate extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,7 +200,7 @@ public class WiredConditionMatchDate extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeMode(int value) {
|
int normalizeMode(int value) {
|
||||||
if (value < MODE_SKIP || value > MODE_RANGE) {
|
if (value < MODE_SKIP || value > MODE_RANGE) {
|
||||||
return MODE_SKIP;
|
return MODE_SKIP;
|
||||||
}
|
}
|
||||||
@@ -201,20 +208,20 @@ public class WiredConditionMatchDate extends InteractionWiredCondition {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeDay(int value) {
|
int normalizeDay(int value) {
|
||||||
return Math.max(1, Math.min(31, value));
|
return Math.max(1, Math.min(31, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeYear(int value) {
|
int normalizeYear(int value) {
|
||||||
return Math.max(1, Math.min(9999, value));
|
return Math.max(1, Math.min(9999, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeWeekdayMask(int value) {
|
int normalizeWeekdayMask(int value) {
|
||||||
int normalized = value & ALL_WEEKDAYS_MASK;
|
int normalized = value & ALL_WEEKDAYS_MASK;
|
||||||
return (normalized == 0) ? ALL_WEEKDAYS_MASK : normalized;
|
return (normalized == 0) ? ALL_WEEKDAYS_MASK : normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeMonthMask(int value) {
|
int normalizeMonthMask(int value) {
|
||||||
int normalized = value & ALL_MONTHS_MASK;
|
int normalized = value & ALL_MONTHS_MASK;
|
||||||
return (normalized == 0) ? ALL_MONTHS_MASK : normalized;
|
return (normalized == 0) ? ALL_MONTHS_MASK : normalized;
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-19
@@ -87,7 +87,7 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
this.direction = params[1] == 1;
|
this.direction = params[1] == 1;
|
||||||
this.position = params[2] == 1;
|
this.position = params[2] == 1;
|
||||||
this.altitude = (params.length > 3) && (params[3] == 1);
|
this.altitude = (params.length > 3) && (params[3] == 1);
|
||||||
this.furniSource = (params.length > 4) ? params[4] : ((params.length > 3 && params[3] > 1) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER);
|
this.furniSource = (params.length > 4) ? WiredMatchPositionInputGuard.normalizeFurniSource(params[4], false) : ((params.length > 3 && params[3] > 1) ? WiredMatchPositionInputGuard.normalizeFurniSource(params[3], false) : WiredSourceUtil.SOURCE_TRIGGER);
|
||||||
this.quantifier = (params.length > 5) ? this.normalizeQuantifier(params[5]) : QUANTIFIER_ALL;
|
this.quantifier = (params.length > 5) ? this.normalizeQuantifier(params[5]) : QUANTIFIER_ALL;
|
||||||
|
|
||||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
|
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
|
||||||
@@ -108,11 +108,17 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY(), item.getZ()));
|
this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY(), item.getZ()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(this.furniSource, !this.settings.isEmpty());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
|
||||||
if (this.settings.isEmpty())
|
if (this.settings.isEmpty())
|
||||||
@@ -126,6 +132,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected boolean evaluateAllTargetsMatch(WiredContext ctx) {
|
protected boolean evaluateAllTargetsMatch(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Room room = ctx.room();
|
Room room = ctx.room();
|
||||||
|
|
||||||
if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) {
|
if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) {
|
||||||
@@ -159,6 +169,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected boolean evaluateAnyTargetMatches(WiredContext ctx) {
|
protected boolean evaluateAnyTargetMatches(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Room room = ctx.room();
|
Room room = ctx.room();
|
||||||
|
|
||||||
if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) {
|
if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) {
|
||||||
@@ -247,44 +261,84 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
|
this.onPickUp();
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.onPickUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.state = data.state;
|
this.state = data.state;
|
||||||
this.position = data.position;
|
this.position = data.position;
|
||||||
this.direction = data.direction;
|
this.direction = data.direction;
|
||||||
this.altitude = data.altitude;
|
this.altitude = data.altitude;
|
||||||
if (data.settings != null) {
|
this.settings.addAll(WiredMatchPositionInputGuard.sanitizeSettings(data.settings, room));
|
||||||
this.settings.addAll(data.settings);
|
this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(data.furniSource, !this.settings.isEmpty());
|
||||||
}
|
|
||||||
this.furniSource = data.furniSource;
|
|
||||||
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
int itemCount = Integer.parseInt(data[0]);
|
if (data.length >= 5) {
|
||||||
|
try {
|
||||||
|
int itemCount = Math.min(Integer.parseInt(data[0]), WiredManager.MAXIMUM_FURNI_SELECTION);
|
||||||
|
|
||||||
String[] items = data[1].split(";");
|
String[] items = data[1].split(";");
|
||||||
|
|
||||||
for (int i = 0; i < itemCount; i++) {
|
for (int i = 0; i < itemCount && i < items.length; i++) {
|
||||||
String[] stuff = items[i].split("-");
|
WiredMatchFurniSetting setting = this.parseLegacySetting(items[i], room);
|
||||||
|
if (setting != null) {
|
||||||
|
this.settings.add(setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (stuff.length >= 6)
|
this.state = data[2].equals("1");
|
||||||
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5])));
|
this.direction = data[3].equals("1");
|
||||||
else if (stuff.length >= 5)
|
this.position = data[4].equals("1");
|
||||||
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4])));
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep whatever was parsed plus defaults
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = data[2].equals("1");
|
|
||||||
this.direction = data[3].equals("1");
|
|
||||||
this.position = data[4].equals("1");
|
|
||||||
this.altitude = false;
|
this.altitude = false;
|
||||||
this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_TRIGGER, !this.settings.isEmpty());
|
||||||
this.quantifier = QUANTIFIER_ALL;
|
this.quantifier = QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WiredMatchFurniSetting parseLegacySetting(String value, Room room) {
|
||||||
|
String[] parts = value.split("-", 6);
|
||||||
|
if (parts.length < 5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
double z = (parts.length >= 6) ? Double.parseDouble(parts[5]) : 0.0D;
|
||||||
|
return WiredMatchPositionInputGuard.sanitizeParts(
|
||||||
|
Integer.parseInt(parts[0]),
|
||||||
|
parts[1],
|
||||||
|
Integer.parseInt(parts[2]),
|
||||||
|
Integer.parseInt(parts[3]),
|
||||||
|
Integer.parseInt(parts[4]),
|
||||||
|
z,
|
||||||
|
room
|
||||||
|
);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPickUp() {
|
public void onPickUp() {
|
||||||
this.settings.clear();
|
this.settings.clear();
|
||||||
@@ -296,10 +350,58 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
this.quantifier = QUANTIFIER_ALL;
|
this.quantifier = QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeQuantifier(int value) {
|
int normalizeQuantifier(int value) {
|
||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int normalizeFurniSource(int value) {
|
||||||
|
switch (value) {
|
||||||
|
case WiredSourceUtil.SOURCE_TRIGGER:
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTED:
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTOR:
|
||||||
|
case WiredSourceUtil.SOURCE_SIGNAL:
|
||||||
|
return value;
|
||||||
|
default:
|
||||||
|
return WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WiredMatchFurniSetting normalizeSetting(WiredMatchFurniSetting setting) {
|
||||||
|
if (setting == null || setting.item_id <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rotation = Math.max(0, Math.min(7, setting.rotation));
|
||||||
|
int x = Math.max(0, setting.x);
|
||||||
|
int y = Math.max(0, setting.y);
|
||||||
|
double z = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, setting.z));
|
||||||
|
|
||||||
|
return new WiredMatchFurniSetting(setting.item_id, setting.state, rotation, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
WiredMatchFurniSetting parseLegacySetting(String[] values) {
|
||||||
|
if (values == null || values.length < 5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int itemId = Integer.parseInt(values[0]);
|
||||||
|
if (itemId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String state = values[1];
|
||||||
|
int rotation = Integer.parseInt(values[2]);
|
||||||
|
int x = Integer.parseInt(values[3]);
|
||||||
|
int y = Integer.parseInt(values[4]);
|
||||||
|
double z = values.length >= 6 ? Double.parseDouble(values[5]) : 0.0D;
|
||||||
|
|
||||||
|
return this.normalizeSetting(new WiredMatchFurniSetting(itemId, state, rotation, x, y, z));
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void refresh() {
|
protected void refresh() {
|
||||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
|
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -126,7 +126,14 @@ public class WiredConditionMatchTime extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -195,7 +202,7 @@ public class WiredConditionMatchTime extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeMode(int value) {
|
int normalizeMode(int value) {
|
||||||
if (value < MODE_SKIP || value > MODE_RANGE) {
|
if (value < MODE_SKIP || value > MODE_RANGE) {
|
||||||
return MODE_SKIP;
|
return MODE_SKIP;
|
||||||
}
|
}
|
||||||
@@ -203,11 +210,11 @@ public class WiredConditionMatchTime extends InteractionWiredCondition {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeHour(int value) {
|
int normalizeHour(int value) {
|
||||||
return Math.max(0, Math.min(23, value));
|
return Math.max(0, Math.min(23, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeMinuteOrSecond(int value) {
|
int normalizeMinuteOrSecond(int value) {
|
||||||
return Math.max(0, Math.min(59, value));
|
return Math.max(0, Math.min(59, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -16,6 +16,7 @@ import java.sql.SQLException;
|
|||||||
|
|
||||||
public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition {
|
public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition {
|
||||||
private static final WiredConditionType type = WiredConditionType.TIME_MORE_THAN;
|
private static final WiredConditionType type = WiredConditionType.TIME_MORE_THAN;
|
||||||
|
static final int MAX_CYCLES = 1_000_000;
|
||||||
|
|
||||||
private int cycles;
|
private int cycles;
|
||||||
|
|
||||||
@@ -52,12 +53,13 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition {
|
|||||||
try {
|
try {
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.cycles = data.cycles;
|
this.cycles = WiredConditionInputGuard.normalizeTimerCycles(data.cycles);
|
||||||
} else {
|
} else {
|
||||||
if (!wiredData.equals(""))
|
if (!wiredData.equals(""))
|
||||||
this.cycles = Integer.parseInt(wiredData);
|
this.cycles = WiredConditionInputGuard.normalizeTimerCycles(Integer.parseInt(wiredData));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
this.cycles = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +92,14 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition {
|
|||||||
@Override
|
@Override
|
||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 1) return false;
|
if(settings.getIntParams().length < 1) return false;
|
||||||
this.cycles = settings.getIntParams()[0];
|
this.cycles = WiredConditionInputGuard.normalizeTimerCycles(settings.getIntParams()[0]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int normalizeCycles(int value) {
|
||||||
|
return Math.max(0, Math.min(MAX_CYCLES, value));
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int cycles;
|
int cycles;
|
||||||
|
|
||||||
|
|||||||
+7
-13
@@ -99,9 +99,9 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.all = data.all;
|
this.all = data.all;
|
||||||
this.furniSource = data.furniSource;
|
this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
|
||||||
|
|
||||||
for (int id : data.itemIds) {
|
for (int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
HabboItem item = room.getHabboItem(id);
|
HabboItem item = room.getHabboItem(id);
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
@@ -115,10 +115,8 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
this.all = (data[0].equals("1"));
|
this.all = (data[0].equals("1"));
|
||||||
|
|
||||||
if (data.length == 2) {
|
if (data.length == 2) {
|
||||||
String[] items = data[1].split(";");
|
for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
|
HabboItem item = room.getHabboItem(id);
|
||||||
for (String s : items) {
|
|
||||||
HabboItem item = room.getHabboItem(Integer.parseInt(s));
|
|
||||||
|
|
||||||
if (item != null)
|
if (item != null)
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
@@ -127,9 +125,7 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||||
}
|
}
|
||||||
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -172,14 +168,12 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
|
|||||||
if(settings.getIntParams().length < 1) return false;
|
if(settings.getIntParams().length < 1) return false;
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.all = params[0] == 1;
|
this.all = params[0] == 1;
|
||||||
this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
|
||||||
int count = settings.getFurniIds().length;
|
int count = settings.getFurniIds().length;
|
||||||
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
|
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
|
||||||
|
|
||||||
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
|
|
||||||
|
|||||||
+8
-14
@@ -95,10 +95,10 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
WiredConditionFurniHaveHabbo.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionFurniHaveHabbo.JsonData.class);
|
WiredConditionFurniHaveHabbo.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionFurniHaveHabbo.JsonData.class);
|
||||||
this.furniSource = data.furniSource;
|
this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
|
||||||
this.all = data.all;
|
this.all = data.all;
|
||||||
|
|
||||||
for(int id : data.itemIds) {
|
for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
HabboItem item = room.getHabboItem(id);
|
HabboItem item = room.getHabboItem(id);
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
@@ -108,11 +108,9 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
if (data.length >= 1) {
|
if (data.length >= 2) {
|
||||||
String[] items = data[1].split(";");
|
for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
|
HabboItem item = room.getHabboItem(id);
|
||||||
for (String s : items) {
|
|
||||||
HabboItem item = room.getHabboItem(Integer.parseInt(s));
|
|
||||||
|
|
||||||
if (item != null)
|
if (item != null)
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
@@ -121,9 +119,7 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||||
this.all = false;
|
this.all = false;
|
||||||
}
|
}
|
||||||
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -161,11 +157,9 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
|
|||||||
|
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.all = (params.length > 0) && (params[0] == 1);
|
this.all = (params.length > 0) && (params[0] == 1);
|
||||||
this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER);
|
this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : ((params.length > 0 && params[0] > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER);
|
||||||
|
|
||||||
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -20,6 +20,10 @@ public class WiredConditionNotFurniTypeMatch extends WiredConditionFurniTypeMatc
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.getQuantifier() == QUANTIFIER_ANY) {
|
if (this.getQuantifier() == QUANTIFIER_ANY) {
|
||||||
return !this.evaluateAllMatches(ctx);
|
return !this.evaluateAllMatches(ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-12
@@ -6,8 +6,8 @@ import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
|
|||||||
import com.eu.habbo.habbohotel.rooms.Room;
|
import com.eu.habbo.habbohotel.rooms.Room;
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomUnit;
|
import com.eu.habbo.habbohotel.rooms.RoomUnit;
|
||||||
import com.eu.habbo.habbohotel.wired.WiredConditionType;
|
import com.eu.habbo.habbohotel.wired.WiredConditionType;
|
||||||
import com.eu.habbo.habbohotel.wired.core.WiredManager;
|
|
||||||
import com.eu.habbo.habbohotel.wired.core.WiredContext;
|
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.habbohotel.wired.core.WiredSourceUtil;
|
||||||
import com.eu.habbo.messages.ServerMessage;
|
import com.eu.habbo.messages.ServerMessage;
|
||||||
|
|
||||||
@@ -31,6 +31,10 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER)
|
int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER)
|
||||||
? ctx.room().getUserCount()
|
? ctx.room().getUserCount()
|
||||||
: WiredSourceUtil.resolveUsers(ctx, this.userSource).size();
|
: WiredSourceUtil.resolveUsers(ctx, this.userSource).size();
|
||||||
@@ -55,25 +59,34 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
|
this.onPickUp();
|
||||||
|
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
WiredConditionHabboCount.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionHabboCount.JsonData.class);
|
WiredConditionHabboCount.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionHabboCount.JsonData.class);
|
||||||
this.lowerLimit = data.lowerLimit;
|
this.applyLimits(data.lowerLimit, data.upperLimit);
|
||||||
this.upperLimit = data.upperLimit;
|
this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource);
|
||||||
this.userSource = data.userSource;
|
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
this.lowerLimit = Integer.parseInt(data[0]);
|
if (data.length >= 2) {
|
||||||
this.upperLimit = Integer.parseInt(data[1]);
|
try {
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.applyLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim()));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep the constructed defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPickUp() {
|
public void onPickUp() {
|
||||||
this.upperLimit = 0;
|
this.lowerLimit = 10;
|
||||||
this.lowerLimit = 20;
|
this.upperLimit = 20;
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,14 +116,19 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
|
|||||||
@Override
|
@Override
|
||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 2) return false;
|
if(settings.getIntParams().length < 2) return false;
|
||||||
this.lowerLimit = settings.getIntParams()[0];
|
|
||||||
this.upperLimit = settings.getIntParams()[1];
|
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.applyLimits(params[0], params[1]);
|
||||||
|
this.userSource = (params.length > 2) ? WiredConditionInputGuard.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyLimits(int lowerLimit, int upperLimit) {
|
||||||
|
int[] limits = WiredConditionInputGuard.normalizeUserCountRange(lowerLimit, upperLimit);
|
||||||
|
this.lowerLimit = limits[0];
|
||||||
|
this.upperLimit = limits[1];
|
||||||
|
}
|
||||||
|
|
||||||
static class JsonData {
|
static class JsonData {
|
||||||
int lowerLimit;
|
int lowerLimit;
|
||||||
int upperLimit;
|
int upperLimit;
|
||||||
|
|||||||
+4
@@ -22,6 +22,10 @@ public class WiredConditionNotMatchStatePosition extends WiredConditionMatchStat
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
|
||||||
if (this.getMatchFurniSettings().isEmpty()) {
|
if (this.getMatchFurniSettings().isEmpty()) {
|
||||||
|
|||||||
+6
-1
@@ -29,6 +29,7 @@ abstract class WiredConditionTeamGameBase extends InteractionWiredCondition {
|
|||||||
protected static final int COMPARISON_EQUAL = 1;
|
protected static final int COMPARISON_EQUAL = 1;
|
||||||
protected static final int COMPARISON_HIGHER = 2;
|
protected static final int COMPARISON_HIGHER = 2;
|
||||||
protected static final int TEAM_TRIGGERER = 0;
|
protected static final int TEAM_TRIGGERER = 0;
|
||||||
|
protected static final int MAX_SCORE = 1_000_000;
|
||||||
|
|
||||||
private static final GameTeamColors[] SUPPORTED_TEAM_COLORS = new GameTeamColors[] {
|
private static final GameTeamColors[] SUPPORTED_TEAM_COLORS = new GameTeamColors[] {
|
||||||
GameTeamColors.RED,
|
GameTeamColors.RED,
|
||||||
@@ -96,7 +97,11 @@ abstract class WiredConditionTeamGameBase extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected int normalizeScore(int value) {
|
protected int normalizeScore(int value) {
|
||||||
return Math.max(0, value);
|
if (value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(value, MAX_SCORE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int normalizeExplicitTeamType(int value) {
|
protected int normalizeExplicitTeamType(int value) {
|
||||||
|
|||||||
+7
-1
@@ -65,7 +65,13 @@ public class WiredConditionTeamHasRank extends WiredConditionTeamGameBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
this.resetSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -66,7 +66,13 @@ public class WiredConditionTeamHasScore extends WiredConditionTeamGameBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
this.resetSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -97,12 +97,12 @@ public class WiredConditionTeamMember extends InteractionWiredCondition {
|
|||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.teamColor = data.teamColor;
|
this.teamColor = WiredConditionInputGuard.normalizeTeamColor(data.teamColor, GameTeamColors.RED);
|
||||||
this.userSource = data.userSource;
|
this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource);
|
||||||
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
|
this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY);
|
||||||
} else {
|
} else {
|
||||||
if (!wiredData.equals(""))
|
if (!wiredData.equals(""))
|
||||||
this.teamColor = GameTeamColors.values()[Integer.parseInt(wiredData)];
|
this.teamColor = WiredConditionInputGuard.normalizeTeamColorType(Integer.parseInt(wiredData), GameTeamColors.RED);
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = QUANTIFIER_ANY;
|
this.quantifier = QUANTIFIER_ANY;
|
||||||
}
|
}
|
||||||
@@ -147,8 +147,8 @@ public class WiredConditionTeamMember extends InteractionWiredCondition {
|
|||||||
public boolean saveData(WiredSettings settings) {
|
public boolean saveData(WiredSettings settings) {
|
||||||
if(settings.getIntParams().length < 1) return false;
|
if(settings.getIntParams().length < 1) return false;
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.teamColor = GameTeamColors.values()[params[0]];
|
this.teamColor = WiredConditionInputGuard.normalizeTeamColorType(params[0], GameTeamColors.RED);
|
||||||
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = (params.length > 1) ? WiredConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ALL) : QUANTIFIER_ANY;
|
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ALL) : QUANTIFIER_ANY;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+33
-16
@@ -42,6 +42,10 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null || ctx.room() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
|
||||||
List<RoomUnit> userTargets = WiredSourceUtil.resolveUsers(ctx, this.userSource);
|
List<RoomUnit> userTargets = WiredSourceUtil.resolveUsers(ctx, this.userSource);
|
||||||
@@ -104,16 +108,19 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
public void loadWiredData(ResultSet set, Room room) throws SQLException {
|
||||||
this.items.clear();
|
this.onPickUp();
|
||||||
String wiredData = set.getString("wired_data");
|
String wiredData = set.getString("wired_data");
|
||||||
|
if (wiredData == null || wiredData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wiredData.startsWith("{")) {
|
if (wiredData.startsWith("{")) {
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
this.furniSource = data.furniSource;
|
this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource);
|
||||||
this.userSource = data.userSource;
|
this.userSource = WiredFurniConditionInputGuard.normalizeUserSource(data.userSource);
|
||||||
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
this.quantifier = this.normalizeQuantifier(data.quantifier);
|
||||||
|
|
||||||
for(int id : data.itemIds) {
|
for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
HabboItem item = room.getHabboItem(id);
|
HabboItem item = room.getHabboItem(id);
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
@@ -121,10 +128,8 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(";");
|
for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(wiredData, WiredManager.MAXIMUM_FURNI_SELECTION)) {
|
||||||
|
HabboItem item = room.getHabboItem(id);
|
||||||
for (String s : data) {
|
|
||||||
HabboItem item = room.getHabboItem(Integer.parseInt(s));
|
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
this.items.add(item);
|
this.items.add(item);
|
||||||
@@ -134,9 +139,7 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
|
|||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = QUANTIFIER_ALL;
|
this.quantifier = QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty());
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -182,13 +185,11 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
|
|||||||
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
|
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
|
||||||
|
|
||||||
int[] params = settings.getIntParams();
|
int[] params = settings.getIntParams();
|
||||||
this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.furniSource = (params.length > 0) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
|
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
|
||||||
|
|
||||||
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
|
this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0);
|
||||||
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
|
|
||||||
@@ -233,6 +234,22 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
|
|||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int normalizeFurniSource(int value) {
|
||||||
|
switch (value) {
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTED:
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTOR:
|
||||||
|
case WiredSourceUtil.SOURCE_SIGNAL:
|
||||||
|
case WiredSourceUtil.SOURCE_TRIGGER:
|
||||||
|
return value;
|
||||||
|
default:
|
||||||
|
return WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int normalizeUserSource(int value) {
|
||||||
|
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WiredConditionOperator operator() {
|
public WiredConditionOperator operator() {
|
||||||
return WiredConditionOperator.AND;
|
return WiredConditionOperator.AND;
|
||||||
|
|||||||
+21
-8
@@ -33,6 +33,7 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
|
|||||||
protected static final int QUANTIFIER_ALL = 0;
|
protected static final int QUANTIFIER_ALL = 0;
|
||||||
protected static final int QUANTIFIER_ANY = 1;
|
protected static final int QUANTIFIER_ANY = 1;
|
||||||
protected static final int SOURCE_SPECIFIED_USERNAME = 101;
|
protected static final int SOURCE_SPECIFIED_USERNAME = 101;
|
||||||
|
protected static final int MAX_USERNAME_LENGTH = 64;
|
||||||
|
|
||||||
public static final WiredConditionType type = WiredConditionType.TRIGGERER_MATCH;
|
public static final WiredConditionType type = WiredConditionType.TRIGGERER_MATCH;
|
||||||
|
|
||||||
@@ -84,7 +85,14 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
this.resetSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -284,7 +292,7 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeEntityType(int value) {
|
int normalizeEntityType(int value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case ENTITY_HABBO:
|
case ENTITY_HABBO:
|
||||||
case ENTITY_PET:
|
case ENTITY_PET:
|
||||||
@@ -295,19 +303,19 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeAvatarMode(int value) {
|
int normalizeAvatarMode(int value) {
|
||||||
return (value == AVATAR_MODE_CERTAIN) ? AVATAR_MODE_CERTAIN : AVATAR_MODE_ANY;
|
return (value == AVATAR_MODE_CERTAIN) ? AVATAR_MODE_CERTAIN : AVATAR_MODE_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeQuantifier(int value) {
|
int normalizeQuantifier(int value) {
|
||||||
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizePrimaryUserSource(int value) {
|
int normalizePrimaryUserSource(int value) {
|
||||||
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeCompareUserSource(int value) {
|
int normalizeCompareUserSource(int value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case WiredSourceUtil.SOURCE_CLICKED_USER:
|
case WiredSourceUtil.SOURCE_CLICKED_USER:
|
||||||
case SOURCE_SPECIFIED_USERNAME:
|
case SOURCE_SPECIFIED_USERNAME:
|
||||||
@@ -317,8 +325,13 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeUsername(String value) {
|
String normalizeUsername(String value) {
|
||||||
return (value == null) ? "" : value.trim();
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = value.trim();
|
||||||
|
return normalized.length() <= MAX_USERNAME_LENGTH ? normalized : normalized.substring(0, MAX_USERNAME_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class MatchResult {
|
protected static class MatchResult {
|
||||||
|
|||||||
+8
-2
@@ -87,7 +87,13 @@ public class WiredConditionUserPerformsAction extends InteractionWiredCondition
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
JsonData data;
|
||||||
|
try {
|
||||||
|
data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
this.resetSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
@@ -253,7 +259,7 @@ public class WiredConditionUserPerformsAction extends InteractionWiredCondition
|
|||||||
}
|
}
|
||||||
|
|
||||||
long timestamp = (Long) timestampValue;
|
long timestamp = (Long) timestampValue;
|
||||||
if ((System.currentTimeMillis() - timestamp) > TRANSIENT_ACTION_WINDOW_MS) {
|
if (!WiredUserActionInputGuard.isRecentTimestamp(timestamp, System.currentTimeMillis(), TRANSIENT_ACTION_WINDOW_MS)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-8
@@ -35,6 +35,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
|
|||||||
private static final int DURATION_UNIT_WEEKS = 5;
|
private static final int DURATION_UNIT_WEEKS = 5;
|
||||||
private static final int DURATION_UNIT_MONTHS = 6;
|
private static final int DURATION_UNIT_MONTHS = 6;
|
||||||
private static final int DURATION_UNIT_YEARS = 7;
|
private static final int DURATION_UNIT_YEARS = 7;
|
||||||
|
static final int MAX_DURATION_AMOUNT = 1_000_000;
|
||||||
|
|
||||||
protected int compareValue = COMPARE_VALUE_CREATED;
|
protected int compareValue = COMPARE_VALUE_CREATED;
|
||||||
protected int comparison = COMPARISON_LOWER_THAN;
|
protected int comparison = COMPARISON_LOWER_THAN;
|
||||||
@@ -97,7 +98,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
|
|||||||
this.targetType = (params.length > 0) ? normalizeTargetTypeExtended(params[0]) : TARGET_USER;
|
this.targetType = (params.length > 0) ? normalizeTargetTypeExtended(params[0]) : TARGET_USER;
|
||||||
this.compareValue = (params.length > 1) ? normalizeCompareValue(params[1]) : COMPARE_VALUE_CREATED;
|
this.compareValue = (params.length > 1) ? normalizeCompareValue(params[1]) : COMPARE_VALUE_CREATED;
|
||||||
this.comparison = (params.length > 2) ? normalizeComparison(params[2]) : COMPARISON_LOWER_THAN;
|
this.comparison = (params.length > 2) ? normalizeComparison(params[2]) : COMPARISON_LOWER_THAN;
|
||||||
this.durationAmount = Math.max(0, (params.length > 3) ? params[3] : 0);
|
this.durationAmount = normalizeDurationAmount((params.length > 3) ? params[3] : 0);
|
||||||
this.durationUnit = (params.length > 4) ? normalizeDurationUnit(params[4]) : DURATION_UNIT_SECONDS;
|
this.durationUnit = (params.length > 4) ? normalizeDurationUnit(params[4]) : DURATION_UNIT_SECONDS;
|
||||||
this.userSource = (params.length > 5) ? normalizeUserSource(params[5]) : WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = (params.length > 5) ? normalizeUserSource(params[5]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
this.furniSource = (params.length > 6) ? normalizeFurniSource(params[6]) : WiredSourceUtil.SOURCE_TRIGGER;
|
this.furniSource = (params.length > 6) ? normalizeFurniSource(params[6]) : WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
@@ -130,6 +131,10 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean evaluate(WiredContext ctx) {
|
public boolean evaluate(WiredContext ctx) {
|
||||||
|
if (ctx == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Room room = ctx.room();
|
Room room = ctx.room();
|
||||||
|
|
||||||
if (room == null || this.variableToken == null || this.variableToken.isEmpty() || !isCustomVariableToken(this.variableToken)) {
|
if (room == null || this.variableToken == null || this.variableToken.isEmpty() || !isCustomVariableToken(this.variableToken)) {
|
||||||
@@ -192,7 +197,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
|
|||||||
this.targetType = normalizeTargetTypeExtended(data.targetType);
|
this.targetType = normalizeTargetTypeExtended(data.targetType);
|
||||||
this.compareValue = normalizeCompareValue(data.compareValue);
|
this.compareValue = normalizeCompareValue(data.compareValue);
|
||||||
this.comparison = normalizeComparison(data.comparison);
|
this.comparison = normalizeComparison(data.comparison);
|
||||||
this.durationAmount = Math.max(0, data.durationAmount);
|
this.durationAmount = normalizeDurationAmount(data.durationAmount);
|
||||||
this.durationUnit = normalizeDurationUnit(data.durationUnit);
|
this.durationUnit = normalizeDurationUnit(data.durationUnit);
|
||||||
this.userSource = normalizeUserSource(data.userSource);
|
this.userSource = normalizeUserSource(data.userSource);
|
||||||
this.furniSource = normalizeFurniSource(data.furniSource);
|
this.furniSource = normalizeFurniSource(data.furniSource);
|
||||||
@@ -345,8 +350,8 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
|
|||||||
return Math.max(0L, System.currentTimeMillis() - timestampMs);
|
return Math.max(0L, System.currentTimeMillis() - timestampMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long durationToMillis(int amount, int unit) {
|
static long durationToMillis(int amount, int unit) {
|
||||||
long normalizedAmount = Math.max(0L, amount);
|
long normalizedAmount = normalizeDurationAmount(amount);
|
||||||
|
|
||||||
return switch (unit) {
|
return switch (unit) {
|
||||||
case DURATION_UNIT_MILLISECONDS -> normalizedAmount;
|
case DURATION_UNIT_MILLISECONDS -> normalizedAmount;
|
||||||
@@ -367,22 +372,26 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable {
|
|||||||
return left * right;
|
return left * right;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int normalizeTargetTypeExtended(int value) {
|
static int normalizeDurationAmount(int value) {
|
||||||
|
return Math.max(0, Math.min(MAX_DURATION_AMOUNT, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int normalizeTargetTypeExtended(int value) {
|
||||||
return switch (value) {
|
return switch (value) {
|
||||||
case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value;
|
case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value;
|
||||||
default -> TARGET_USER;
|
default -> TARGET_USER;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int normalizeCompareValue(int value) {
|
static int normalizeCompareValue(int value) {
|
||||||
return (value == COMPARE_VALUE_UPDATED) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED;
|
return (value == COMPARE_VALUE_UPDATED) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int normalizeComparison(int value) {
|
static int normalizeComparison(int value) {
|
||||||
return (value == COMPARISON_HIGHER_THAN) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN;
|
return (value == COMPARISON_HIGHER_THAN) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int normalizeDurationUnit(int value) {
|
static int normalizeDurationUnit(int value) {
|
||||||
return switch (value) {
|
return switch (value) {
|
||||||
case DURATION_UNIT_MILLISECONDS, DURATION_UNIT_SECONDS, DURATION_UNIT_MINUTES, DURATION_UNIT_HOURS,
|
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;
|
DURATION_UNIT_DAYS, DURATION_UNIT_WEEKS, DURATION_UNIT_MONTHS, DURATION_UNIT_YEARS -> value;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user