You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 44ea3abd4e | |||
| 609cd20ab2 | |||
| 717a7f184f | |||
| 2862446686 | |||
| e97e680006 | |||
| 7e59dca192 | |||
| 1109d53720 | |||
| f12363a5b7 | |||
| 7d4ffec74e | |||
| 281fede58c | |||
| edf152485b | |||
| 18a1bfbe90 | |||
| 7c32bbfd2d | |||
| 4eae206b64 | |||
| 155b2202c7 | |||
| 10c291eb9f | |||
| 349a8c727e | |||
| 68f2b71d14 | |||
| 69a6c0d060 | |||
| 9f36d95dbc | |||
| 885bdca0c4 | |||
| db035294a7 | |||
| 3216ba1df6 | |||
| c9a47b1fac | |||
| 8d6b969d75 | |||
| b9723e0298 | |||
| c4aae676b2 | |||
| 7624d3fbc3 | |||
| 585f4dd3aa | |||
| afa114d511 | |||
| e9129576a9 | |||
| 0aadd01493 | |||
| 9d98fbf9ee | |||
| b38274e134 | |||
| 02ab30180c | |||
| da63439d53 | |||
| bf1a29a6e8 | |||
| 6391d721ff | |||
| dfea6bcf83 | |||
| a7f207bb76 | |||
| b7915884b6 | |||
| 478f7bdba0 | |||
| c255f1e1b4 | |||
| 9c831a9da4 | |||
| 08d1ae97a7 | |||
| f8fe1e3e22 | |||
| be77cdf4aa | |||
| 1ba2e43d4d | |||
| 8dd5155562 | |||
| 4f4f581371 | |||
| 9705b3e42a | |||
| e626a7fc50 | |||
| d6ebb632e6 | |||
| 014ca9ca48 | |||
| d189d66f9e | |||
| c272a36cc5 | |||
| 1d6e05ee57 | |||
| ea44771d69 | |||
| 1da783aff9 | |||
| e772686c4b | |||
| a00f7b01f5 | |||
| 6b4089cace | |||
| 9ea7acf05c | |||
| bab43af41e | |||
| 10a2b2b872 | |||
| 458b37dbed | |||
| 55b38e7b85 | |||
| 4a96c5baaf | |||
| 539c5b5b96 | |||
| 7b7154e68f | |||
| 4aabb738a3 | |||
| 691dc42627 | |||
| 226873c1fb | |||
| a06a204b39 | |||
| e213609609 | |||
| 44d38b8661 | |||
| ccadb81970 | |||
| 0a3a940946 | |||
| 4613fbe80c | |||
| 9328f4a355 | |||
| da8b947ddf | |||
| b9658d0407 | |||
| 68d3731393 | |||
| 4ef4ed1a96 | |||
| c20e273a2c | |||
| 83d418e712 | |||
| 38d05e8a06 | |||
| c83a3bad8e | |||
| 0bd23ad244 | |||
| d8f74b2477 | |||
| dac09e92d1 | |||
| fbf979419e | |||
| 6126c35779 | |||
| a1749c9eda | |||
| 525c124fa5 | |||
| 57087a31f2 | |||
| c4b3295a45 | |||
| 418c753e6c | |||
| 8419f11883 | |||
| 1a0d783ff7 | |||
| 655e039df7 | |||
| 7726691cde | |||
| 67503aeb2a | |||
| b206b32748 | |||
| ad60861a3f | |||
| b77290f5e7 | |||
| b14730d37f | |||
| 9126396973 | |||
| d321ff3b85 | |||
| 7f38a25eef | |||
| 4820ab15f3 | |||
| 8d989e7a19 | |||
| 1f7ec96e1c | |||
| 969f177108 | |||
| e485c2747c | |||
| d99a51899b | |||
| 29677a19be | |||
| 21ee36e089 | |||
| 4e47dbee16 | |||
| e7ba4d0926 | |||
| 67d2f52f64 | |||
| 69d770b65e | |||
| 2492569e16 | |||
| 9c215bea6b | |||
| 7dc3581f8f | |||
| f38eb32eee | |||
| 222e356ff0 | |||
| c8022ccc45 | |||
| 9579833775 | |||
| 87ad289a54 | |||
| fd28af5f69 | |||
| 99c938b98f | |||
| 82d90418cd | |||
| 8b51be4940 | |||
| 54259f89bd | |||
| 272a9b9f42 | |||
| 9c94402f78 |
@@ -39,5 +39,8 @@ DEALLOCATE PREPARE stmt;
|
||||
UPDATE emulator_settings SET `key`='ws.whitelist' WHERE `key`='websockets.whitelist';
|
||||
UPDATE emulator_settings SET `key`='ws.host' WHERE `key`='ws.nitro.host';
|
||||
UPDATE emulator_settings SET `key`='ws.port' WHERE `key`='ws.nitro.port';
|
||||
INSERT emulator_settings (`key`, `value`) VALUES ('ws.ip.header', 'X-Forwarded-For');
|
||||
INSERT emulator_settings (`key`, `value`) VALUES ('ws.enabled', 'true');
|
||||
INSERT IGNORE INTO emulator_settings (`key`, `value`)
|
||||
VALUES ('ws.ip.header', 'X-Forwarded-For');
|
||||
|
||||
INSERT IGNORE INTO emulator_settings (`key`, `value`)
|
||||
VALUES ('ws.enabled', 'true');
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN `background_border_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`;
|
||||
|
||||
ALTER TABLE infostand_backgrounds
|
||||
CHANGE COLUMN `category` `category` ENUM('background', 'stand', 'overlay', 'card', 'border') NOT NULL ;
|
||||
|
||||
|
||||
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
|
||||
(1, 'border', 1, 0, 0),
|
||||
(2, 'border', 1, 0, 0),
|
||||
(3, 'border', 1, 0, 0),
|
||||
(4, 'border', 1, 0, 0),
|
||||
(5, 'border', 1, 0, 0),
|
||||
(6, 'border', 1, 0, 0),
|
||||
(7, 'border', 1, 0, 0),
|
||||
(8, 'border', 1, 0, 0),
|
||||
(9, 'border', 1, 0, 0),
|
||||
(10, 'border', 1, 0, 0),
|
||||
(11, 'border', 1, 0, 0),
|
||||
(12, 'border', 1, 0, 0),
|
||||
(13, 'border', 1, 0, 0),
|
||||
(14, 'border', 1, 0, 0),
|
||||
(15, 'border', 1, 0, 0),
|
||||
(16, 'border', 1, 0, 0),
|
||||
(17, 'border', 1, 0, 0),
|
||||
(18, 'border', 1, 0, 0),
|
||||
(19, 'border', 1, 0, 0),
|
||||
(20, 'border', 1, 0, 0),
|
||||
(21, 'border', 1, 0, 0),
|
||||
(22, 'border', 1, 0, 0),
|
||||
(23, 'border', 1, 0, 0),
|
||||
(24, 'border', 1, 0, 0),
|
||||
(25, 'border', 1, 0, 0);
|
||||
@@ -0,0 +1,474 @@
|
||||
-- ============================================================
|
||||
-- Live required schema
|
||||
-- ============================================================
|
||||
-- Consolidated schema for the currently used Nitro/Arcturus live
|
||||
-- additions. This file intentionally excludes old/unused migration
|
||||
-- artifacts and dump-only data.
|
||||
--
|
||||
-- Scope:
|
||||
-- - tables/columns currently referenced by Java code
|
||||
-- - runtime settings required by secure assets/API, login, wired, and UI
|
||||
-- - safe CREATE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS statements
|
||||
--
|
||||
-- Assumes the base Arcturus database already exists.
|
||||
-- Tested for MariaDB-style syntax used by this project.
|
||||
-- ============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Core settings support
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
ALTER TABLE `emulator_settings`
|
||||
ADD COLUMN IF NOT EXISTS `comment` TEXT NULL DEFAULT '' AFTER `value`;
|
||||
|
||||
ALTER TABLE catalog_pages
|
||||
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `includes`;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
|
||||
`key` VARCHAR(255) NOT NULL,
|
||||
`value` TEXT NOT NULL,
|
||||
`comment` TEXT NULL DEFAULT '',
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('crypto.ws.enabled', '0'),
|
||||
('crypto.ws.signing.enabled', '0'),
|
||||
('crypto.ws.signing.public_key', ''),
|
||||
('crypto.ws.signing.private_key', ''),
|
||||
('login.access.jwt.secret', ''),
|
||||
('login.remember.duration.days', '30'),
|
||||
('login.remember.rotate.interval.minutes', '15'),
|
||||
('login.remember.jwt.secret', ''),
|
||||
('login.turnstile.enabled', '0'),
|
||||
('login.turnstile.sitekey', ''),
|
||||
('login.turnstile.secretkey', ''),
|
||||
('login.ratelimit.enabled', '1'),
|
||||
('login.ratelimit.max_attempts', '5'),
|
||||
('login.ratelimit.window_sec', '60'),
|
||||
('login.ratelimit.lockout_sec', '120'),
|
||||
('login.register.enabled', '1'),
|
||||
('register.max_per_ip', '5'),
|
||||
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
|
||||
('register.default.motto', 'I love Habbo!'),
|
||||
('password.reset.url', 'http://localhost/reset-password'),
|
||||
('smtp.provider', 'own'),
|
||||
('smtp.host', 'localhost'),
|
||||
('smtp.port', '587'),
|
||||
('smtp.username', ''),
|
||||
('smtp.password', ''),
|
||||
('smtp.from_address', 'no-reply@example.com'),
|
||||
('smtp.from_name', 'Habbo Hotel'),
|
||||
('smtp.use_tls', '1'),
|
||||
('smtp.use_ssl', '0'),
|
||||
('new_user_credits', '0'),
|
||||
('new_user_duckets', '0'),
|
||||
('new_user_diamonds', '0')
|
||||
ON DUPLICATE KEY UPDATE `value` = `value`;
|
||||
|
||||
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('wired.engine.enabled', '1', 'Compatibility flag. The runtime uses the new wired engine.'),
|
||||
('wired.engine.exclusive', '1', 'Compatibility flag. The runtime uses exclusive wired engine execution.'),
|
||||
('wired.engine.maxStepsPerStack', '100', 'Maximum internal processing steps allowed for a single wired stack execution.'),
|
||||
('wired.engine.debug', '0', 'Enable verbose debug logging for the wired engine.'),
|
||||
('wired.custom.enabled', '0', 'Enable custom legacy wired compatibility behavior.'),
|
||||
('hotel.wired.furni.selection.count', '5', 'Maximum number of furni that a wired box can store or select.'),
|
||||
('hotel.wired.max_delay', '20', 'Maximum delay value accepted by wired effects that support delayed execution.'),
|
||||
('hotel.wired.message.max_length', '512', 'Maximum length of wired message text fields.'),
|
||||
('wired.effect.teleport.delay', '500', 'Delay in milliseconds used by wired teleport movement.'),
|
||||
('wired.place.under', '0', 'Allow placing wired furniture underneath other items when room rules permit it.'),
|
||||
('wired.tick.interval.ms', '50', 'Global wired tick interval in milliseconds.'),
|
||||
('wired.tick.resolution', '100', 'Legacy wired tick resolution value.'),
|
||||
('wired.tick.debug', '0', 'Enable verbose logging for the wired tick service.'),
|
||||
('wired.tick.thread.priority', '6', 'Java thread priority used by the wired tick service.'),
|
||||
('wired.highscores.displaycount', '25', 'Maximum number of wired highscore entries shown to users.'),
|
||||
('wired.abuse.max.recursion.depth', '10', 'Maximum recursive wired depth before execution is stopped.'),
|
||||
('wired.abuse.max.events.per.window', '100', 'Maximum identical wired events allowed inside the abuse rate-limit window.'),
|
||||
('wired.abuse.rate.limit.window.ms', '10000', 'Wired abuse rate-limit window in milliseconds.'),
|
||||
('wired.abuse.ban.duration.ms', '600000', 'Temporary wired ban duration after abuse detection.'),
|
||||
('wired.monitor.usage.window.ms', '1000', 'Rolling window size for wired usage monitoring.'),
|
||||
('wired.monitor.usage.limit', '1000', 'Maximum wired usage budget in one monitor window.'),
|
||||
('wired.monitor.delayed.events.limit', '100', 'Maximum delayed wired events queued in one room.'),
|
||||
('wired.monitor.overload.average.ms', '50', 'Average execution time threshold for overload tracking.'),
|
||||
('wired.monitor.overload.peak.ms', '150', 'Peak execution time threshold for overload tracking.'),
|
||||
('wired.monitor.overload.consecutive.windows', '2', 'Consecutive overloaded windows required before logging overload.'),
|
||||
('wired.monitor.heavy.usage.percent', '70', 'Usage percentage threshold for heavy-room tracking.'),
|
||||
('wired.monitor.heavy.consecutive.windows', '5', 'Consecutive windows above heavy usage threshold.'),
|
||||
('wired.monitor.heavy.delayed.percent', '60', 'Delayed queue percentage threshold for heavy-room tracking.')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`value` = VALUES(`value`),
|
||||
`comment` = VALUES(`comment`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Login API, room templates, remember-me, and news
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `password_resets` (
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`token` VARCHAR(128) NOT NULL,
|
||||
`expires_at` TIMESTAMP NOT NULL,
|
||||
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`user_id`),
|
||||
UNIQUE KEY `idx_password_resets_token` (`token`),
|
||||
CONSTRAINT `fk_password_resets_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_remember_families` (
|
||||
`family_id` CHAR(36) NOT NULL,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`current_version` INT(11) NOT NULL DEFAULT 1,
|
||||
`created_at` INT(11) NOT NULL,
|
||||
`expires_at` INT(11) NOT NULL,
|
||||
`revoked` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`last_ip` VARCHAR(45) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`family_id`),
|
||||
KEY `idx_users_remember_families_user_id` (`user_id`),
|
||||
KEY `idx_users_remember_families_expires_at` (`expires_at`),
|
||||
CONSTRAINT `fk_users_remember_families_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_templates` (
|
||||
`template_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`description` VARCHAR(256) NOT NULL DEFAULT '',
|
||||
`thumbnail` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
`enabled` ENUM('0','1') NOT NULL DEFAULT '1',
|
||||
`name` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`room_description` VARCHAR(250) NOT NULL DEFAULT '',
|
||||
`model` VARCHAR(100) NOT NULL,
|
||||
`password` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`state` ENUM('open','locked','password','invisible') NOT NULL DEFAULT 'open',
|
||||
`users_max` INT(11) NOT NULL DEFAULT 25,
|
||||
`category` INT(11) NOT NULL DEFAULT 0,
|
||||
`paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||
`paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||
`paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||
`thickness_wall` INT(11) NOT NULL DEFAULT 0,
|
||||
`thickness_floor` INT(11) NOT NULL DEFAULT 0,
|
||||
`moodlight_data` VARCHAR(2048) NOT NULL DEFAULT '',
|
||||
`override_model` ENUM('0','1') NOT NULL DEFAULT '0',
|
||||
`trade_mode` INT(2) NOT NULL DEFAULT 2,
|
||||
`heightmap` MEDIUMTEXT NOT NULL,
|
||||
`door_x` INT(11) NOT NULL DEFAULT 0,
|
||||
`door_y` INT(11) NOT NULL DEFAULT 0,
|
||||
`door_dir` INT(4) NOT NULL DEFAULT 2,
|
||||
PRIMARY KEY (`template_id`),
|
||||
KEY `idx_room_templates_enabled_sort` (`enabled`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_templates_items` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_id` INT(11) NOT NULL,
|
||||
`item_id` INT(11) UNSIGNED NOT NULL,
|
||||
`wall_pos` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`x` INT(11) NOT NULL DEFAULT 0,
|
||||
`y` INT(11) NOT NULL DEFAULT 0,
|
||||
`z` DOUBLE(10,6) NOT NULL DEFAULT 0.000000,
|
||||
`rot` INT(11) NOT NULL DEFAULT 0,
|
||||
`extra_data` VARCHAR(2096) NOT NULL DEFAULT '',
|
||||
`wired_data` VARCHAR(4096) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_room_templates_items_template_id` (`template_id`),
|
||||
KEY `idx_room_templates_items_item_id` (`item_id`),
|
||||
CONSTRAINT `fk_room_templates_items_template`
|
||||
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_room_templates_items_item_base`
|
||||
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ui_news` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(150) NOT NULL,
|
||||
`body` TEXT NOT NULL,
|
||||
`image` MEDIUMTEXT DEFAULT NULL,
|
||||
`link_text` VARCHAR(80) NOT NULL DEFAULT '',
|
||||
`link_url` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ui_news_enabled_sort` (`enabled`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
INSERT INTO `ui_news` (`title`, `body`, `image`, `link_text`, `link_url`, `enabled`, `sort_order`)
|
||||
SELECT 'Welcome to the Hotel!', 'Catch up on the latest events, updates and competitions happening right now in the hotel.', '', '', '', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `ui_news`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Wired runtime data
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`inspect_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
|
||||
`modify_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
|
||||
PRIMARY KEY (`room_id`),
|
||||
CONSTRAINT `fk_room_wired_settings_room`
|
||||
FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`variable_item_id` INT(11) NOT NULL,
|
||||
`value` INT(11) DEFAULT NULL,
|
||||
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`variable_item_id` INT(11) NOT NULL,
|
||||
`value` INT(11) DEFAULT NULL,
|
||||
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
|
||||
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_user_wired_variables_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`furni_id` INT(11) NOT NULL,
|
||||
`variable_item_id` INT(11) NOT NULL,
|
||||
`value` INT(11) DEFAULT NULL,
|
||||
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
|
||||
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- User customization: prefixes, nick icons, profile backgrounds
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
ALTER TABLE `users`
|
||||
ADD COLUMN IF NOT EXISTS `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`,
|
||||
ADD COLUMN IF NOT EXISTS `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`,
|
||||
ADD COLUMN IF NOT EXISTS `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`,
|
||||
ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `infostand_backgrounds` (
|
||||
`id` INT(11) NOT NULL,
|
||||
`category` ENUM('background','stand','overlay','card') NOT NULL,
|
||||
`min_rank` INT(11) NOT NULL DEFAULT 0,
|
||||
`is_hc_only` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`is_ambassador_only` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`, `category`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
|
||||
(0, 'background', 0, 0, 0),
|
||||
(0, 'stand', 0, 0, 0),
|
||||
(0, 'overlay', 0, 0, 0),
|
||||
(0, 'card', 0, 0, 0);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_prefixes` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`text` VARCHAR(50) NOT NULL,
|
||||
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
|
||||
`icon` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`effect` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`font` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`points` INT(11) NOT NULL DEFAULT 0,
|
||||
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`active` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_prefixes_user_id` (`user_id`),
|
||||
KEY `idx_user_prefixes_user_active` (`user_id`, `active`),
|
||||
CONSTRAINT `fk_user_prefixes_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`text` VARCHAR(50) NOT NULL,
|
||||
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
|
||||
`icon` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`effect` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`font` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`points` INT(11) NOT NULL DEFAULT 0,
|
||||
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
|
||||
PRIMARY KEY (`user_id`),
|
||||
CONSTRAINT `fk_user_visual_settings_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
|
||||
`key_name` VARCHAR(100) NOT NULL,
|
||||
`value` VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (`key_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
|
||||
('max_length', '15'),
|
||||
('min_rank_to_buy', '1'),
|
||||
('price_credits', '5'),
|
||||
('price_points', '0'),
|
||||
('points_type', '0'),
|
||||
('font_price_credits', '10'),
|
||||
('font_price_points', '0'),
|
||||
('font_points_type', '0');
|
||||
|
||||
INSERT IGNORE INTO `custom_prefixes_catalog`
|
||||
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`)
|
||||
VALUES
|
||||
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
|
||||
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
|
||||
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`icon_key` VARCHAR(50) NOT NULL,
|
||||
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`points` INT(11) NOT NULL DEFAULT 0,
|
||||
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_custom_nick_icons_catalog_icon_key` (`icon_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_nick_icons` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`icon_key` VARCHAR(50) NOT NULL,
|
||||
`active` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_nick_icons_user_icon` (`user_id`, `icon_key`),
|
||||
KEY `idx_user_nick_icons_user_id` (`user_id`),
|
||||
KEY `idx_user_nick_icons_user_active` (`user_id`, `active`),
|
||||
CONSTRAINT `fk_user_nick_icons_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
|
||||
('1', 'Icon 1', 10, 0, 1, 1),
|
||||
('2', 'Icon 2', 10, 0, 1, 2),
|
||||
('3', 'Icon 3', 10, 0, 1, 3),
|
||||
('4', 'Icon 4', 10, 0, 1, 4),
|
||||
('5', 'Icon 5', 10, 0, 1, 5),
|
||||
('6', 'Icon 6', 10, 0, 1, 6);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Custom badge maker
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`badge_path` VARCHAR(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584',
|
||||
`badge_url` VARCHAR(255) NOT NULL DEFAULT '/gamedata/c_images/album1584',
|
||||
`price_badge` INT(11) NOT NULL DEFAULT 0,
|
||||
`currency_type` INT(11) NOT NULL DEFAULT -1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`)
|
||||
SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_custom_badge` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`badge_id` VARCHAR(64) NOT NULL,
|
||||
`badge_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`badge_description` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`date_created` INT(11) NOT NULL DEFAULT 0,
|
||||
`date_edit` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_custom_badge_badge_id` (`badge_id`),
|
||||
KEY `idx_user_custom_badge_user_id` (`user_id`),
|
||||
CONSTRAINT `fk_user_custom_badge_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- UI/catalog compatibility values used by the current client
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
|
||||
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
|
||||
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
|
||||
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
|
||||
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
|
||||
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
|
||||
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
|
||||
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
|
||||
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
|
||||
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
|
||||
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
|
||||
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
|
||||
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
|
||||
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
|
||||
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
|
||||
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
|
||||
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
|
||||
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
|
||||
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
|
||||
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`permission` = VALUES(`permission`),
|
||||
`overridable` = VALUES(`overridable`),
|
||||
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
|
||||
|
||||
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||
('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', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
||||
('commands.error.cmd_setroom_template', 'Could not save room as template. Check the server log for details.'),
|
||||
('commands.error.cmd_setroom_template.no_room', 'You must be inside a room to use this command.'),
|
||||
('commands.keys.cmd_give_prefix', 'giveprefix'),
|
||||
('commands.keys.cmd_list_prefixes', 'listprefixes'),
|
||||
('commands.keys.cmd_remove_prefix', 'removeprefix'),
|
||||
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
|
||||
('wiredfurni.badgereceived.body', 'You have just received a new Badge! Check your Inventory!'),
|
||||
('wiredfurni.badgereceived.title', 'Badge received!');
|
||||
|
||||
-- Optional permission metadata for normalized permission schemas.
|
||||
-- Actual rank values still belong in the permissions/permission_ranks setup.
|
||||
CREATE TABLE IF NOT EXISTS `permission_definitions` (
|
||||
`permission_key` VARCHAR(64) NOT NULL,
|
||||
`max_value` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`comment` TEXT NOT NULL,
|
||||
PRIMARY KEY (`permission_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`) VALUES
|
||||
('cmd_setroom_template', 1, 'Allows using :setroom_template to copy a room into the login room-template table.'),
|
||||
('cmd_give_prefix', 1, 'Allows granting custom prefixes to users.'),
|
||||
('cmd_list_prefixes', 1, 'Allows listing custom prefixes assigned to users.'),
|
||||
('cmd_remove_prefix', 1, 'Allows removing custom prefixes from users.'),
|
||||
('cmd_prefix_blacklist', 1, 'Allows managing the custom prefix blacklist.')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`max_value` = VALUES(`max_value`),
|
||||
`comment` = VALUES(`comment`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Explicitly obsolete table from older remember-me attempts.
|
||||
-- The current Java uses users_remember_families only.
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
DROP TABLE IF EXISTS `users_remember_tokens`;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- ============================================================
|
||||
-- Fix: acc_supporttool wrongly granted to VIP / wrongly denied to Super Mod
|
||||
-- ============================================================
|
||||
-- The default permission_definitions seed shipped acc_supporttool
|
||||
-- with rank pattern (0, 1, 1, 1, 1, 0, 1) — i.e. rank_2 (VIP) and
|
||||
-- rank_3 (X, junior helper) had ALLOWED, while rank_6 (Super Mod)
|
||||
-- did NOT. That's two bugs:
|
||||
--
|
||||
-- * VIP users see the ModTools button on the toolbar and can
|
||||
-- open Room/User info windows. The actual sanction endpoints
|
||||
-- still gate on ACC_SUPPORTTOOL server-side so they can't
|
||||
-- actually moderate, but the UI exposure is wrong and lets a
|
||||
-- VIP request user info / room info / chatlogs they have no
|
||||
-- business reading.
|
||||
-- * Super Mod is denied the tool entirely, which is obviously
|
||||
-- unintended given the rank name.
|
||||
--
|
||||
-- Intended pattern: only Support (4) and up — (0, 0, 0, 1, 1, 1, 1).
|
||||
--
|
||||
-- Run on existing deployments to align with the corrected default
|
||||
-- seed in `Default Database/FullDatabase.sql`. Idempotent.
|
||||
|
||||
UPDATE `permission_definitions`
|
||||
SET `rank_1` = 0,
|
||||
`rank_2` = 0,
|
||||
`rank_3` = 0,
|
||||
`rank_4` = 1,
|
||||
`rank_5` = 1,
|
||||
`rank_6` = 1,
|
||||
`rank_7` = 1
|
||||
WHERE `permission_key` = 'acc_supporttool';
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE catalog_club_offers
|
||||
ADD COLUMN IF NOT EXISTS giftable ENUM('0','1') NOT NULL DEFAULT '0';
|
||||
|
||||
INSERT INTO emulator_texts (`key`, `value`)
|
||||
VALUES ('prereg.reward.you.received', 'You have recived:'),
|
||||
('generic.days', 'days');
|
||||
@@ -0,0 +1,17 @@
|
||||
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`, `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) VALUES ('acc_housekeeping', '1', 'Allow housekeeping in the client', '0', '0', '0', '0', '0', '0', '1');
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `housekeeping_log` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`timestamp` INT NOT NULL,
|
||||
`actor_id` INT NOT NULL,
|
||||
`actor_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||
`target_id` INT NOT NULL DEFAULT 0,
|
||||
`target_label` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`action` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`detail` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`success` TINYINT NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `timestamp` (`timestamp`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,70 @@
|
||||
ALTER TABLE `bots`
|
||||
MODIFY COLUMN `type` ENUM('generic','visitor_log','bartender','weapons_dealer','frank')
|
||||
NOT NULL DEFAULT 'generic';
|
||||
|
||||
INSERT INTO `permission_definitions`
|
||||
(`permission_key`, `max_value`, `comment`,
|
||||
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
|
||||
VALUES
|
||||
('acc_bot_frank', 1, 'Required to purchase the Frank mascot bot from the catalog.',
|
||||
0, 0, 0, 0, 0, 0, 1)
|
||||
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `bot_chat_responses` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`bot_type` VARCHAR(32) NOT NULL,
|
||||
`keys` VARCHAR(255) NOT NULL COMMENT 'semicolon-separated trigger words',
|
||||
`responses` TEXT NOT NULL COMMENT 'newline-separated replies; bot picks one at random',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `bot_type` (`bot_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
|
||||
('frank', '__door_triggers', 'show me the door\nkick me\ni want to leave\nlet me out'),
|
||||
('frank', '__door_lines', 'Right this way - mind the step!\nAnd out you go. Come back soon!\nAllow me to escort you to the exit.\nThere''s the door. Farewell, true believer!'),
|
||||
('frank', '__busy_whisper', 'Sorry, I am currently busy. Please wait until I am available.'),
|
||||
('frank', 'frank', 'Hello, I''m Frank! Welcome to Habbo.'),
|
||||
('frank', 'help', 'What do you need help with?'),
|
||||
('frank', 'thanks;thank you', 'Just doing my job, true believer!'),
|
||||
('frank', 'new', 'Welcome to Habbo! I hope you have a great time here.'),
|
||||
('frank', 'rooms', 'Looking for somewhere fun? Try the Navigator - thousands of rooms to explore!'),
|
||||
('frank', 'sulake', 'Sulake is the company behind Habbo. Take a look: https://www.sulake.com'),
|
||||
('frank', 'vip;hc', 'VIP gets you more outfits, more furni, more everything. Worth it!'),
|
||||
('frank', 'music', 'Snoop Dogg, Frank Sinatra and a little Beethoven on Sundays.'),
|
||||
('frank', 'movie', 'I''m a Casablanca man. Black and white films are an underrated art.'),
|
||||
('frank', 'game', 'Battleship. Always Battleship.'),
|
||||
('frank', 'snowstorm', 'Honestly? I''m terrible at Snowstorm. Don''t tell anyone.'),
|
||||
('frank', 'furni', 'Best furniture maker in town - hands down, the folks at Sulake.'),
|
||||
('frank', 'animal;cat;pet','I have a cat called Mr. Whiskers. He runs the place, really.'),
|
||||
('frank', 'miranda', 'Miranda. The love of my life. Don''t get me started.'),
|
||||
('frank', 'frank black', 'Named after the man himself. Frank Black is a hero of mine.'),
|
||||
('frank', 'life', 'Life is like a bowl of popcorn - warm, salty and buttery.'),
|
||||
('frank', 'job;work', 'I''m sure you can find work in one of the guest rooms!'),
|
||||
('frank', 'snouthill', 'Snouthill... so many memories.'),
|
||||
('frank', 'wife', 'I had a wife once. She broke my stereo.'),
|
||||
('frank', 'baseball', 'Oh, I used to love to go down to the old ball park and watch Christy Mathewson and Honus Wagner at bat.'),
|
||||
('frank', 'mark', 'I don''t trust Mark.'),
|
||||
('frank', 'vietnam', 'Vietnam? Don''t ask. Worst trip of my life.'),
|
||||
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
|
||||
|
||||
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
|
||||
('sunflower', 1002),
|
||||
('cola;habbo cola', 19),
|
||||
('rose', 1000),
|
||||
('book', 1003),
|
||||
('tea', 27),
|
||||
('coffee', 8),
|
||||
('migraine;headache;pills', 1015),
|
||||
('radioactive liquid;radioactive', 30),
|
||||
('turkey;can of turkey', 70);
|
||||
|
||||
-- VERY IMPORTANT !!!!
|
||||
-- First check if the items_base ID and catalog_items ID is not in use !
|
||||
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
|
||||
|
||||
INSERT IGNORE INTO `items_base` (`id`, `sprite_id`, `item_name`, `public_name`, `width`, `length`, `stack_height`, `allow_stack`, `allow_sit`, `allow_lay`, `allow_walk`, `allow_gift`, `allow_trade`, `allow_recycle`, `allow_marketplace_sell`, `allow_inventory_stack`, `type`, `interaction_type`, `interaction_modes_count`, `vending_ids`, `multiheight`, `customparams`)
|
||||
VALUES (19001, 0, 'bot_frank', 'Frank', 1, 1, 0.00, '0', '0', '0', '1', '0', '0', '0', '0', '0', 'r', 'default', 1, '0', '0', '0');
|
||||
|
||||
INSERT IGNORE INTO `catalog_items` (`item_ids`, `page_id`, `offer_id`, `catalog_name`, `cost_credits`, `cost_points`, `points_type`, `amount`, `extradata`)
|
||||
VALUES ('19001', 8, 19001, 'Frank', 0, 0, 0, 1, 'name:Frank;motto:Welcome to Habbo!;figure:hr-3499-33.sh-290-90.ch-3971-72-73.lg-270-73.hd-205-1-1.fa-1206-67.ha-3409-73-72;gender:m');
|
||||
@@ -0,0 +1,89 @@
|
||||
ALTER TABLE `rooms`
|
||||
ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `soundboard_sounds` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client
|
||||
`url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges)
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Fortune Wheel — tables
|
||||
-- ----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `wheel_prizes` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`type` VARCHAR(16) NOT NULL DEFAULT 'nothing', -- item | badge | credits | points | spin | nothing
|
||||
`value` VARCHAR(64) NOT NULL DEFAULT '', -- item: base item id ; badge: badge code ; others: unused
|
||||
`amount` INT(11) NOT NULL DEFAULT 1, -- item qty / credits / points / extra spins
|
||||
`points_type` INT(11) NOT NULL DEFAULT 5, -- for type=points (diamond default 5)
|
||||
`weight` INT(11) NOT NULL DEFAULT 1, -- relative probability
|
||||
`label` VARCHAR(64) NOT NULL DEFAULT '', -- slice label override (optional)
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wheel_user_state` (
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`free_spins` INT(11) NOT NULL DEFAULT 0, -- remaining free spins for the current day
|
||||
`extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins
|
||||
`last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400)
|
||||
PRIMARY KEY (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wheel_recent_wins` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`username` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`look` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`prize_label` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`won_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('wheel.free_spins_per_day', '1', 'Fortune wheel: free spins granted each day.'),
|
||||
('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.'),
|
||||
('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).')
|
||||
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
|
||||
|
||||
INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`)
|
||||
SELECT `type`, `amount`, `points_type`, `weight`, `label`, `sort_order`
|
||||
FROM (
|
||||
SELECT 'points' AS `type`, 25 AS `amount`, 5 AS `points_type`, 20 AS `weight`, '25 diamonds' AS `label`, 1 AS `sort_order`
|
||||
UNION ALL SELECT 'points', 50, 5, 12, '50 diamonds', 2
|
||||
UNION ALL SELECT 'points', 200, 5, 3, '200 diamonds', 3
|
||||
UNION ALL SELECT 'credits', 100, 0, 15, '100 credits', 4
|
||||
UNION ALL SELECT 'spin', 1, 0, 15, '1 Extra spin', 5
|
||||
UNION ALL SELECT 'spin', 2, 0, 6, '2 Extra spins', 6
|
||||
UNION ALL SELECT 'nothing', 0, 0, 29, 'Oh to bad!', 7
|
||||
) AS seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `wheel_prizes`);
|
||||
|
||||
INSERT IGNORE INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
|
||||
VALUES (
|
||||
'acc_wheeladmin',
|
||||
1,
|
||||
'Allows opening the Fortune Wheel prize editor (FortuneWheelSettingsView) to add/edit prize slices. Gated server-side by the same key.'
|
||||
);
|
||||
|
||||
SET @cols := NULL;
|
||||
SELECT GROUP_CONCAT(CONCAT('dst.`', `column_name`, '` = src.`', `column_name`, '`') SEPARATOR ', ')
|
||||
INTO @cols
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'permission_definitions'
|
||||
AND `column_name` REGEXP '^rank_[0-9]+$';
|
||||
|
||||
SET @sql := CONCAT(
|
||||
'UPDATE `permission_definitions` dst ',
|
||||
'JOIN `permission_definitions` src ON src.`permission_key` = ''acc_ads_background'' ',
|
||||
'SET ', @cols, ' ',
|
||||
'WHERE dst.`permission_key` = ''acc_wheeladmin'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -0,0 +1,89 @@
|
||||
CREATE TABLE IF NOT EXISTS `habbo_mentions` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`target_user_id` INT(11) NOT NULL,
|
||||
`sender_user_id` INT(11) NOT NULL,
|
||||
`sender_username` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`room_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`room_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`message` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`mention_type` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0 = direct (@nick), 1 = broadcast (@all/@friends/@room)',
|
||||
`timestamp` INT(11) NOT NULL DEFAULT 0,
|
||||
`read` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_target_id` (`target_user_id`, `id`),
|
||||
KEY `idx_target_unread` (`target_user_id`, `read`),
|
||||
KEY `idx_target_timestamp` (`target_user_id`, `timestamp`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
|
||||
|
||||
INSERT INTO `permission_definitions`
|
||||
(`permission_key`, `max_value`, `comment`,
|
||||
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`, `rank_8`)
|
||||
VALUES
|
||||
('acc_mention_everyone', 1,
|
||||
'Allow sending @all / @everyone / @tutti broadcast mentions (hotel-wide).',
|
||||
0, 0, 0, 0, 1, 1, 1, 1),
|
||||
('acc_mention_friends', 1,
|
||||
'Allow sending @friends / @amici broadcast mentions (sender''s online buddies).',
|
||||
0, 0, 0, 0, 1, 1, 1, 1),
|
||||
('cmd_disablementions', 1,
|
||||
'Allow toggling :disablementions to stop receiving any @mention notifications.',
|
||||
1, 1, 1, 1, 1, 1, 1, 1),
|
||||
('cmd_disablemassmentions', 1,
|
||||
'Allow toggling :disablemassmentions to stop receiving broadcast mentions (direct @nick still works).',
|
||||
1, 1, 1, 1, 1, 1, 1, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`comment` = VALUES(`comment`);
|
||||
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 3. Emulator settings: cooldowns, caps and alias lists
|
||||
--
|
||||
-- Only inserted when missing - existing tuned values are preserved.
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('mentions.enabled', '1',
|
||||
'Master switch. 1 = process @mentions, 0 = disable the feature entirely.'),
|
||||
('mentions.max.targets', '50',
|
||||
'Hard cap on how many users a single broadcast (@all / @friends / @room) can fan out to.'),
|
||||
('mentions.cooldown.ms', '3000',
|
||||
'Per-sender cooldown between any two mentions, in milliseconds.'),
|
||||
('mentions.room.cooldown.ms', '15000',
|
||||
'Extra per-sender cooldown for broadcast mentions (@all / @friends / @room) on top of mentions.cooldown.ms.'),
|
||||
('mentions.store.limit', '50',
|
||||
'Number of mentions returned in the initial RequestMentionsList response.'),
|
||||
('mentions.request.cooldown.ms', '2000',
|
||||
'Per-user cooldown between RequestMentionsList packets.'),
|
||||
('mentions.markread.cooldown.ms', '500',
|
||||
'Per-user cooldown between mark-single-as-read packets.'),
|
||||
('mentions.markall.cooldown.ms', '5000',
|
||||
'Per-user cooldown between mark-all-as-read packets (bulk DB update).'),
|
||||
('mentions.delete.cooldown.ms', '500',
|
||||
'Per-user cooldown between delete-mention packets.'),
|
||||
('mentions.everyone.aliases', 'all,everyone,tutti',
|
||||
'Comma-separated aliases that trigger an @everyone broadcast (requires acc_mention_everyone).'),
|
||||
('mentions.friends.aliases', 'friends,amici',
|
||||
'Comma-separated aliases that trigger an @friends broadcast (requires acc_mention_friends).'),
|
||||
('mentions.room.aliases', 'room,stanza',
|
||||
'Comma-separated aliases that trigger an @room broadcast (no permission required, room scope only).');
|
||||
|
||||
|
||||
ALTER TABLE `wordfilter`
|
||||
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`;
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 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 ?');
|
||||
@@ -63,15 +63,6 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
|
||||
PRIMARY KEY (`key_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5. Blacklist table
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`word` VARCHAR(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_word` (`word`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- Schema upgrades for existing installations
|
||||
@@ -296,14 +287,6 @@ INSERT IGNORE INTO `custom_prefixes_catalog`
|
||||
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
|
||||
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
|
||||
|
||||
-- ============================================================
|
||||
-- Example blacklist entries
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
|
||||
('admin'),
|
||||
('staff'),
|
||||
('mod'),
|
||||
('owner');
|
||||
|
||||
-- ============================================================
|
||||
-- Notes
|
||||
|
||||
@@ -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';
|
||||
@@ -28598,7 +28598,7 @@ INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`,
|
||||
('acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.', 0, 0, 0, 0, 0, 0, 1),
|
||||
('acc_staff_pick', 1, 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.', 0, 0, 0, 0, 0, 0, 1),
|
||||
('acc_superwired', 1, 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.', 0, 0, 0, 0, 0, 0, 1),
|
||||
('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 1, 1, 1, 1, 0, 1),
|
||||
('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 0, 0, 1, 1, 1, 1),
|
||||
('acc_trade_anywhere', 1, 'Allows starting trades outside the normal trade-enabled areas.', 0, 0, 0, 0, 0, 0, 1),
|
||||
('acc_unignorable', 1, 'Prevents the account from being ignored by other users through the ignore system.', 0, 0, 0, 0, 0, 0, 0),
|
||||
('acc_unkickable', 1, 'Prevents the user from being kicked by normal moderation or room commands.', 0, 0, 0, 0, 0, 0, 1),
|
||||
|
||||
+27
-19
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.eu.habbo</groupId>
|
||||
<artifactId>Habbo</artifactId>
|
||||
<version>4.2.10</version>
|
||||
<version>4.2.43</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -38,6 +38,7 @@
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.eu.habbo.Emulator</mainClass>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
@@ -61,6 +62,12 @@
|
||||
<show>public</show>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.2</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
@@ -76,21 +83,21 @@
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
<version>4.1.115.Final</version>
|
||||
<version>4.2.15.Final</version>
|
||||
</dependency>
|
||||
|
||||
<!-- GSON -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.11.0</version>
|
||||
<version>2.14.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MariaDB Connector/J (native driver for MariaDB) -->
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.5.8</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -106,7 +113,7 @@
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>6.2.1</version>
|
||||
<version>7.0.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -114,7 +121,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.17.0</version>
|
||||
<version>3.20.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -130,7 +137,7 @@
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.18.3</version>
|
||||
<version>1.22.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -138,14 +145,14 @@
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.16</version>
|
||||
<version>2.0.18</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logback -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.5.15</version>
|
||||
<version>1.5.34</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -153,14 +160,7 @@
|
||||
<dependency>
|
||||
<groupId>org.fusesource.jansi</groupId>
|
||||
<artifactId>jansi</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Joda Time -->
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
<version>2.13.0</version>
|
||||
<version>2.4.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- jBCrypt � used by the built-in /api/auth/* HTTP login handler
|
||||
@@ -171,12 +171,20 @@
|
||||
<version>0.4</version>
|
||||
</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 -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.angus</groupId>
|
||||
<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>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -6,6 +6,7 @@ import ch.qos.logback.core.ConsoleAppender;
|
||||
import com.eu.habbo.core.*;
|
||||
import com.eu.habbo.core.consolecommands.ConsoleCommand;
|
||||
import com.eu.habbo.database.Database;
|
||||
import com.eu.habbo.gui.EmulatorDashboard;
|
||||
import com.eu.habbo.habbohotel.GameEnvironment;
|
||||
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
|
||||
import com.eu.habbo.networking.gameserver.GameServer;
|
||||
@@ -38,12 +39,23 @@ public final class Emulator {
|
||||
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");
|
||||
|
||||
// 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.
|
||||
public final static int MAJOR = 4;
|
||||
public final static int MINOR = 1;
|
||||
public final static int BUILD = 0;
|
||||
public final static String PREVIEW = "";
|
||||
|
||||
public static final String version = "Arcturus Morningstar" + " " + MAJOR + "." + MINOR + "." + BUILD + " " + PREVIEW;
|
||||
// Tied to the Maven project version: read from the jar manifest
|
||||
// (Implementation-Version = ${project.version}, see pom assembly plugin).
|
||||
private static String resolveVersionNumber() {
|
||||
String implementation = Emulator.class.getPackage().getImplementationVersion();
|
||||
if (implementation != null && !implementation.isEmpty()) return implementation;
|
||||
String fallback = MAJOR + "." + MINOR + "." + BUILD;
|
||||
return PREVIEW.isEmpty() ? fallback : fallback + " " + PREVIEW;
|
||||
}
|
||||
|
||||
public static final String version = "Arcturus Morningstar Extended " + resolveVersionNumber();
|
||||
private static final String logo =
|
||||
"\n" +
|
||||
"███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" +
|
||||
@@ -186,6 +198,10 @@ public final class Emulator {
|
||||
Emulator.isReady = true;
|
||||
Emulator.timeStarted = getIntUnixTimestamp();
|
||||
|
||||
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
|
||||
EmulatorDashboard.launch();
|
||||
}
|
||||
|
||||
if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) {
|
||||
LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.",
|
||||
Emulator.getConfig().getInt("runtime.threads"),
|
||||
|
||||
@@ -50,6 +50,7 @@ public class RoomUserPetComposer extends MessageComposer {
|
||||
this.response.appendString("");
|
||||
this.response.appendString("unknown");
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
return this.response;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,14 @@ class DatabasePool {
|
||||
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
|
||||
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "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.setPoolName("HabboHikariPool");
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
package com.eu.habbo.gui;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.monitoring.EmulatorStatsService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.CompoundBorder;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.MatteBorder;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class EmulatorDashboard extends JFrame {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(EmulatorDashboard.class);
|
||||
|
||||
// Modern Dark Theme Colors
|
||||
private static final Color COLOR_BG = new Color(18, 18, 18);
|
||||
private static final Color COLOR_SURFACE = new Color(30, 30, 30);
|
||||
private static final Color COLOR_SURFACE_HOVER = new Color(45, 45, 45);
|
||||
private static final Color COLOR_PRIMARY = new Color(99, 102, 241);
|
||||
private static final Color COLOR_PRIMARY_SOFT = new Color(99, 102, 241, 45);
|
||||
private static final Color COLOR_SUCCESS = new Color(34, 197, 94);
|
||||
private static final Color COLOR_WARNING = new Color(245, 158, 11);
|
||||
private static final Color COLOR_TEXT = new Color(240, 240, 240);
|
||||
private static final Color COLOR_TEXT_MUTED = new Color(150, 150, 150);
|
||||
private static final Color COLOR_TEXT_SUBTLE = new Color(110, 110, 110);
|
||||
private static final Color COLOR_BORDER = new Color(50, 50, 50);
|
||||
private static final Font FONT_TITLE = new Font("Segoe UI", Font.BOLD, 26);
|
||||
private static final Font FONT_SECTION = new Font("Segoe UI", Font.BOLD, 16);
|
||||
private static final Font FONT_SMALL = new Font("Segoe UI", Font.PLAIN, 12);
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
|
||||
private static EmulatorDashboard instance;
|
||||
|
||||
// Overview Tab
|
||||
private final JLabel memLabel = createMetricLabel();
|
||||
private final JLabel cpuLabel = createMetricLabel();
|
||||
private final JLabel threadLabel = createMetricLabel();
|
||||
private final JLabel usersLabel = createMetricLabel();
|
||||
private final JLabel roomsLabel = createMetricLabel();
|
||||
private final JLabel wiredLabel = createMetricLabel();
|
||||
private final JLabel uptimeLabel = createStatusValueLabel();
|
||||
private final JLabel lastUpdatedLabel = createStatusValueLabel();
|
||||
private final JLabel footerStatusLabel = createStatusValueLabel();
|
||||
private final MemoryGraphPanel memoryGraph = new MemoryGraphPanel();
|
||||
|
||||
// Tables
|
||||
private final DefaultTableModel usersTableModel;
|
||||
private final DefaultTableModel roomsTableModel;
|
||||
private final DefaultTableModel wiredTableModel;
|
||||
private final JTable usersTable;
|
||||
private final JTable roomsTable;
|
||||
private final JTable wiredTable;
|
||||
private final JLabel usersCountLabel = createCountLabel();
|
||||
private final JLabel roomsCountLabel = createCountLabel();
|
||||
private final JLabel wiredCountLabel = createCountLabel();
|
||||
|
||||
// UI Components
|
||||
private final JPanel cardsPanel;
|
||||
private final CardLayout cardLayout;
|
||||
private final Map<String, JPanel> navButtons = new HashMap<>();
|
||||
private String selectedCardName = "Overview";
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "Dashboard-Updater");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private EmulatorDashboard() {
|
||||
setTitle("Arcturus Morningstar - System Dashboard");
|
||||
setSize(1100, 700);
|
||||
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||
setLocationRelativeTo(null);
|
||||
getContentPane().setBackground(COLOR_BG);
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
// Setup custom Look & Feel basics to remove weird Swing borders
|
||||
UIManager.put("ScrollBar.background", COLOR_BG);
|
||||
UIManager.put("ScrollBar.thumb", COLOR_SURFACE_HOVER);
|
||||
|
||||
// Sidebar
|
||||
JPanel sidebar = new JPanel();
|
||||
sidebar.setLayout(new BoxLayout(sidebar, BoxLayout.Y_AXIS));
|
||||
sidebar.setBackground(COLOR_SURFACE);
|
||||
sidebar.setPreferredSize(new Dimension(220, 0));
|
||||
sidebar.setBorder(new MatteBorder(0, 0, 0, 1, COLOR_BORDER));
|
||||
|
||||
// Sidebar Header
|
||||
JPanel brandPanel = new JPanel(new BorderLayout());
|
||||
brandPanel.setBackground(COLOR_SURFACE);
|
||||
brandPanel.setBorder(new EmptyBorder(20, 20, 30, 20));
|
||||
|
||||
JLabel brandTitle = new JLabel("Arcturus");
|
||||
brandTitle.setFont(new Font("Segoe UI", Font.BOLD, 22));
|
||||
brandTitle.setForeground(COLOR_TEXT);
|
||||
|
||||
JLabel brandSub = new JLabel("v" + Emulator.version);
|
||||
brandSub.setFont(new Font("Segoe UI", Font.PLAIN, 12));
|
||||
brandSub.setForeground(COLOR_PRIMARY);
|
||||
|
||||
brandPanel.add(brandTitle, BorderLayout.NORTH);
|
||||
brandPanel.add(brandSub, BorderLayout.SOUTH);
|
||||
sidebar.add(brandPanel);
|
||||
|
||||
// Main Cards
|
||||
cardLayout = new CardLayout();
|
||||
cardsPanel = new JPanel(cardLayout);
|
||||
cardsPanel.setBackground(COLOR_BG);
|
||||
|
||||
// Setup Tabs
|
||||
usersTableModel = createTableModel(new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
|
||||
roomsTableModel = createTableModel(new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
|
||||
wiredTableModel = createTableModel(new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
|
||||
usersTable = createDashboardTable(usersTableModel);
|
||||
roomsTable = createDashboardTable(roomsTableModel);
|
||||
wiredTable = createDashboardTable(wiredTableModel);
|
||||
|
||||
cardsPanel.add(createOverviewTab(), "Overview");
|
||||
cardsPanel.add(createTableTab("Online Users", "Players currently connected to the emulator.", usersTable, usersCountLabel), "Online Users");
|
||||
cardsPanel.add(createTableTab("Active Rooms", "Loaded rooms with lightweight performance indicators.", roomsTable, roomsCountLabel), "Active Rooms");
|
||||
cardsPanel.add(createTableTab("Wired Diagnostics", "Rooms currently using wired timing, delay and execution budget.", wiredTable, wiredCountLabel), "Wired Diagnostics");
|
||||
|
||||
// Sidebar Navigation
|
||||
sidebar.add(createNavButton("Overview", "Overview"));
|
||||
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
sidebar.add(createNavButton("Online Users", "Online Users"));
|
||||
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
sidebar.add(createNavButton("Active Rooms", "Active Rooms"));
|
||||
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
sidebar.add(createNavButton("Wired Diagnostics", "Wired Diagnostics"));
|
||||
sidebar.add(Box.createVerticalGlue());
|
||||
|
||||
add(sidebar, BorderLayout.WEST);
|
||||
add(cardsPanel, BorderLayout.CENTER);
|
||||
add(createStatusBar(), BorderLayout.SOUTH);
|
||||
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentShown(ComponentEvent e) {
|
||||
setActiveCard("Overview");
|
||||
}
|
||||
});
|
||||
|
||||
// Start updates
|
||||
scheduler.scheduleAtFixedRate(this::updateMetrics, 0, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private DefaultTableModel createTableModel(String[] cols) {
|
||||
return new DefaultTableModel(cols, 0) {
|
||||
@Override public boolean isCellEditable(int row, int column) { return false; }
|
||||
};
|
||||
}
|
||||
|
||||
private JPanel createNavButton(String text, String cardName) {
|
||||
JPanel btn = new JPanel(new BorderLayout());
|
||||
btn.setBackground(COLOR_SURFACE);
|
||||
btn.setMaximumSize(new Dimension(220, 45));
|
||||
btn.setBorder(new EmptyBorder(0, 18, 0, 0));
|
||||
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||
navButtons.put(cardName, btn);
|
||||
|
||||
JLabel lbl = new JLabel(text);
|
||||
lbl.setFont(new Font("Segoe UI", Font.BOLD, 14));
|
||||
lbl.setForeground(COLOR_TEXT_MUTED);
|
||||
btn.add(lbl, BorderLayout.CENTER);
|
||||
|
||||
btn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
btn.setBackground(COLOR_SURFACE_HOVER);
|
||||
lbl.setForeground(COLOR_TEXT);
|
||||
}
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
updateNavButtonStyle(cardName, btn, lbl);
|
||||
}
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
setActiveCard(cardName);
|
||||
}
|
||||
});
|
||||
|
||||
updateNavButtonStyle(cardName, btn, lbl);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private JPanel createOverviewTab() {
|
||||
JPanel wrapper = new JPanel(new BorderLayout());
|
||||
wrapper.setBackground(COLOR_BG);
|
||||
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
|
||||
|
||||
JPanel header = new JPanel(new BorderLayout(0, 14));
|
||||
header.setOpaque(false);
|
||||
|
||||
JLabel title = new JLabel("Dashboard Overview");
|
||||
title.setFont(FONT_TITLE);
|
||||
title.setForeground(COLOR_TEXT);
|
||||
|
||||
JLabel subtitle = new JLabel("Operational view of emulator health, activity and wired performance.");
|
||||
subtitle.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||
subtitle.setForeground(COLOR_TEXT_MUTED);
|
||||
|
||||
JPanel titleBlock = new JPanel();
|
||||
titleBlock.setOpaque(false);
|
||||
titleBlock.setLayout(new BoxLayout(titleBlock, BoxLayout.Y_AXIS));
|
||||
titleBlock.add(title);
|
||||
titleBlock.add(Box.createRigidArea(new Dimension(0, 4)));
|
||||
titleBlock.add(subtitle);
|
||||
|
||||
header.add(titleBlock, BorderLayout.NORTH);
|
||||
header.add(createOverviewMetaPanel(), BorderLayout.SOUTH);
|
||||
wrapper.add(header, BorderLayout.NORTH);
|
||||
|
||||
JPanel content = new JPanel(new GridLayout(1, 2, 20, 20));
|
||||
content.setOpaque(false);
|
||||
content.setBorder(new EmptyBorder(20, 0, 0, 0));
|
||||
|
||||
// Left Stats
|
||||
JPanel statsPanel = new JPanel(new GridLayout(3, 2, 12, 12));
|
||||
statsPanel.setOpaque(false);
|
||||
statsPanel.add(createMetricCard("Memory Allocation", memLabel));
|
||||
statsPanel.add(createMetricCard("CPU Load", cpuLabel));
|
||||
statsPanel.add(createMetricCard("Active OS Threads", threadLabel));
|
||||
statsPanel.add(createMetricCard("Connected Players", usersLabel));
|
||||
statsPanel.add(createMetricCard("Loaded Rooms", roomsLabel));
|
||||
statsPanel.add(createMetricCard("Wired Tickables", wiredLabel));
|
||||
content.add(statsPanel);
|
||||
|
||||
// Right Graph
|
||||
JPanel graphContainer = new JPanel(new BorderLayout());
|
||||
graphContainer.setBackground(COLOR_SURFACE);
|
||||
graphContainer.setBorder(BorderFactory.createCompoundBorder(
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||
new EmptyBorder(15, 15, 15, 15)
|
||||
));
|
||||
|
||||
JLabel gTitle = new JLabel("Realtime Memory Usage");
|
||||
gTitle.setFont(FONT_SECTION);
|
||||
gTitle.setForeground(COLOR_TEXT_MUTED);
|
||||
gTitle.setBorder(new EmptyBorder(0, 0, 15, 0));
|
||||
graphContainer.add(gTitle, BorderLayout.NORTH);
|
||||
graphContainer.add(memoryGraph, BorderLayout.CENTER);
|
||||
content.add(graphContainer);
|
||||
|
||||
wrapper.add(content, BorderLayout.CENTER);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private JPanel createMetricCard(String title, JLabel valueLabel) {
|
||||
JPanel card = new JPanel(new BorderLayout());
|
||||
card.setBackground(COLOR_SURFACE);
|
||||
card.setBorder(BorderFactory.createCompoundBorder(
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||
new EmptyBorder(15, 20, 15, 20)
|
||||
));
|
||||
|
||||
JLabel tLabel = new JLabel(title);
|
||||
tLabel.setFont(new Font("Segoe UI", Font.BOLD, 13));
|
||||
tLabel.setForeground(COLOR_TEXT_MUTED);
|
||||
|
||||
card.add(tLabel, BorderLayout.NORTH);
|
||||
card.add(valueLabel, BorderLayout.SOUTH);
|
||||
return card;
|
||||
}
|
||||
|
||||
private JLabel createMetricLabel() {
|
||||
JLabel label = new JLabel("-");
|
||||
label.setFont(new Font("Segoe UI", Font.BOLD, 28));
|
||||
label.setForeground(COLOR_TEXT);
|
||||
return label;
|
||||
}
|
||||
|
||||
private JPanel createOverviewMetaPanel() {
|
||||
JPanel panel = new JPanel(new GridLayout(1, 3, 12, 12));
|
||||
panel.setOpaque(false);
|
||||
panel.add(createStatusCard("Uptime", uptimeLabel, COLOR_PRIMARY));
|
||||
panel.add(createStatusCard("Last Refresh", lastUpdatedLabel, COLOR_SUCCESS));
|
||||
panel.add(createStatusCard("GUI Status", footerStatusLabel, COLOR_WARNING));
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createStatusCard(String title, JLabel valueLabel, Color accent) {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.setBackground(COLOR_SURFACE);
|
||||
panel.setBorder(BorderFactory.createCompoundBorder(
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||
new EmptyBorder(12, 14, 12, 14)
|
||||
));
|
||||
|
||||
JPanel accentBar = new JPanel();
|
||||
accentBar.setBackground(accent);
|
||||
accentBar.setPreferredSize(new Dimension(6, 0));
|
||||
|
||||
JPanel content = new JPanel();
|
||||
content.setOpaque(false);
|
||||
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
|
||||
|
||||
JLabel label = new JLabel(title);
|
||||
label.setFont(FONT_SMALL);
|
||||
label.setForeground(COLOR_TEXT_MUTED);
|
||||
|
||||
content.add(label);
|
||||
content.add(Box.createRigidArea(new Dimension(0, 6)));
|
||||
content.add(valueLabel);
|
||||
|
||||
panel.add(accentBar, BorderLayout.WEST);
|
||||
panel.add(content, BorderLayout.CENTER);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JLabel createStatusValueLabel() {
|
||||
JLabel label = new JLabel("-");
|
||||
label.setFont(new Font("Segoe UI", Font.BOLD, 16));
|
||||
label.setForeground(COLOR_TEXT);
|
||||
return label;
|
||||
}
|
||||
|
||||
private JLabel createCountLabel() {
|
||||
JLabel label = new JLabel("0 rows");
|
||||
label.setFont(FONT_SMALL);
|
||||
label.setForeground(COLOR_TEXT_MUTED);
|
||||
return label;
|
||||
}
|
||||
|
||||
private JTable createDashboardTable(DefaultTableModel model) {
|
||||
JTable table = new JTable(model);
|
||||
table.setBackground(COLOR_SURFACE);
|
||||
table.setForeground(COLOR_TEXT);
|
||||
table.setGridColor(COLOR_BORDER);
|
||||
table.setRowHeight(34);
|
||||
table.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||
table.setFillsViewportHeight(true);
|
||||
table.setSelectionBackground(COLOR_PRIMARY);
|
||||
table.setSelectionForeground(Color.WHITE);
|
||||
table.setShowVerticalLines(false);
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setAutoCreateRowSorter(true);
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setBackground(new Color(22, 22, 22));
|
||||
header.setForeground(COLOR_TEXT_MUTED);
|
||||
header.setFont(new Font("Segoe UI", Font.BOLD, 13));
|
||||
header.setPreferredSize(new Dimension(0, 38));
|
||||
header.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, COLOR_BORDER));
|
||||
((DefaultTableCellRenderer) header.getDefaultRenderer()).setHorizontalAlignment(JLabel.LEFT);
|
||||
((DefaultTableCellRenderer) header.getDefaultRenderer()).setBorder(new EmptyBorder(0, 10, 0, 0));
|
||||
|
||||
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
label.setBorder(new EmptyBorder(0, 10, 0, 10));
|
||||
label.setForeground(isSelected ? Color.WHITE : COLOR_TEXT);
|
||||
label.setBackground(isSelected ? COLOR_PRIMARY : ((row % 2 == 0) ? COLOR_SURFACE : new Color(35, 35, 35)));
|
||||
return label;
|
||||
}
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private JPanel createTableTab(String title, String subtitle, JTable table, JLabel countLabel) {
|
||||
JPanel wrapper = new JPanel(new BorderLayout());
|
||||
wrapper.setBackground(COLOR_BG);
|
||||
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
|
||||
|
||||
JPanel titlePanel = new JPanel(new BorderLayout());
|
||||
titlePanel.setOpaque(false);
|
||||
|
||||
JLabel titleLbl = new JLabel(title);
|
||||
titleLbl.setFont(FONT_TITLE);
|
||||
titleLbl.setForeground(COLOR_TEXT);
|
||||
|
||||
JLabel subtitleLbl = new JLabel(subtitle);
|
||||
subtitleLbl.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||
subtitleLbl.setForeground(COLOR_TEXT_MUTED);
|
||||
subtitleLbl.setBorder(new EmptyBorder(6, 0, 0, 0));
|
||||
|
||||
JPanel textPanel = new JPanel();
|
||||
textPanel.setOpaque(false);
|
||||
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS));
|
||||
textPanel.add(titleLbl);
|
||||
textPanel.add(subtitleLbl);
|
||||
|
||||
titlePanel.add(textPanel, BorderLayout.WEST);
|
||||
titlePanel.add(countLabel, BorderLayout.EAST);
|
||||
wrapper.add(titlePanel, BorderLayout.NORTH);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
scrollPane.getViewport().setBackground(COLOR_SURFACE);
|
||||
scrollPane.setBorder(new MatteBorder(1, 1, 1, 1, COLOR_BORDER));
|
||||
scrollPane.setBorder(new CompoundBorder(
|
||||
new EmptyBorder(20, 0, 0, 0),
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER)
|
||||
));
|
||||
wrapper.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private JPanel createStatusBar() {
|
||||
JPanel statusBar = new JPanel(new BorderLayout());
|
||||
statusBar.setBackground(COLOR_SURFACE);
|
||||
statusBar.setBorder(new CompoundBorder(
|
||||
new MatteBorder(1, 0, 0, 0, COLOR_BORDER),
|
||||
new EmptyBorder(8, 14, 8, 14)
|
||||
));
|
||||
|
||||
JLabel left = new JLabel("Dashboard running locally");
|
||||
left.setFont(FONT_SMALL);
|
||||
left.setForeground(COLOR_TEXT_SUBTLE);
|
||||
|
||||
JLabel right = new JLabel("Tip: table columns are sortable");
|
||||
right.setFont(FONT_SMALL);
|
||||
right.setForeground(COLOR_TEXT_SUBTLE);
|
||||
|
||||
statusBar.add(left, BorderLayout.WEST);
|
||||
statusBar.add(right, BorderLayout.EAST);
|
||||
return statusBar;
|
||||
}
|
||||
|
||||
private void setActiveCard(String cardName) {
|
||||
selectedCardName = cardName;
|
||||
cardLayout.show(cardsPanel, cardName);
|
||||
navButtons.forEach((name, button) -> {
|
||||
JLabel label = (JLabel) button.getComponent(0);
|
||||
updateNavButtonStyle(name, button, label);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateNavButtonStyle(String cardName, JPanel button, JLabel label) {
|
||||
boolean active = cardName.equals(selectedCardName);
|
||||
button.setBackground(active ? COLOR_PRIMARY_SOFT : COLOR_SURFACE);
|
||||
label.setForeground(active ? COLOR_TEXT : COLOR_TEXT_MUTED);
|
||||
}
|
||||
|
||||
private void updateMetrics() {
|
||||
try {
|
||||
EmulatorStatsService.Snapshot snapshot = EmulatorStatsService.collectSnapshot();
|
||||
EmulatorStatsService.Overview overview = snapshot.overview;
|
||||
|
||||
Object[][] usersData = new Object[snapshot.users.size()][5];
|
||||
for (int i = 0; i < snapshot.users.size(); i++) {
|
||||
EmulatorStatsService.OnlineUserRow user = snapshot.users.get(i);
|
||||
usersData[i] = new Object[]{user.id, user.username, user.rank, user.credits, user.roomId};
|
||||
}
|
||||
|
||||
Object[][] roomsData = new Object[snapshot.rooms.size()][8];
|
||||
for (int i = 0; i < snapshot.rooms.size(); i++) {
|
||||
EmulatorStatsService.ActiveRoomRow room = snapshot.rooms.get(i);
|
||||
roomsData[i] = new Object[]{
|
||||
room.roomId,
|
||||
room.name,
|
||||
room.players,
|
||||
room.items,
|
||||
room.tickables,
|
||||
String.format("%.2f", room.cpuMs),
|
||||
room.estimatedRamKb,
|
||||
room.thread
|
||||
};
|
||||
}
|
||||
|
||||
Object[][] wiredData = new Object[snapshot.wired.size()][7];
|
||||
for (int i = 0; i < snapshot.wired.size(); i++) {
|
||||
EmulatorStatsService.WiredRoomRow wiredRoom = snapshot.wired.get(i);
|
||||
wiredData[i] = new Object[]{
|
||||
wiredRoom.roomId,
|
||||
wiredRoom.averageTickMs + " ms",
|
||||
wiredRoom.peakTickMs + " ms",
|
||||
wiredRoom.usagePercent + "%",
|
||||
wiredRoom.delayedEventsPending,
|
||||
wiredRoom.overloaded ? "YES" : "NO",
|
||||
wiredRoom.heavy ? "YES" : "NO"
|
||||
};
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
memLabel.setText(String.format("%d MB / %d MB", overview.memoryUsedMb, overview.memoryMaxMb));
|
||||
cpuLabel.setText(String.format("%.1f %%", overview.cpuLoadPercent));
|
||||
threadLabel.setText(String.valueOf(overview.activeOsThreads));
|
||||
usersLabel.setText(String.valueOf(overview.connectedPlayers));
|
||||
roomsLabel.setText(String.valueOf(overview.loadedRooms));
|
||||
wiredLabel.setText(String.valueOf(overview.wiredTickables));
|
||||
uptimeLabel.setText(EmulatorStatsService.formatDuration(overview.uptimeSeconds));
|
||||
lastUpdatedLabel.setText(LocalDateTime.now().format(TIME_FORMAT));
|
||||
footerStatusLabel.setText(overview.guiStatus);
|
||||
memoryGraph.addValue((long) overview.memoryUsedMb * 1024L * 1024L, (long) overview.memoryMaxMb * 1024L * 1024L);
|
||||
|
||||
usersTableModel.setDataVector(usersData, new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
|
||||
roomsTableModel.setDataVector(roomsData, new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
|
||||
wiredTableModel.setDataVector(wiredData, new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
|
||||
usersCountLabel.setText(snapshot.users.size() + " rows");
|
||||
roomsCountLabel.setText(snapshot.rooms.size() + " rows");
|
||||
wiredCountLabel.setText(snapshot.wired.size() + " rows");
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error updating dashboard metrics", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void launch() {
|
||||
if (instance == null) {
|
||||
instance = new EmulatorDashboard();
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
instance.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
private static class MemoryGraphPanel extends JPanel {
|
||||
private final LinkedList<Double> history = new LinkedList<>();
|
||||
private static final int MAX_POINTS = 100;
|
||||
|
||||
public MemoryGraphPanel() {
|
||||
setOpaque(false);
|
||||
}
|
||||
|
||||
public void addValue(long used, long max) {
|
||||
double percent = (double) used / (double) max;
|
||||
history.addLast(percent);
|
||||
if (history.size() > MAX_POINTS) {
|
||||
history.removeFirst();
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
Graphics2D g2 = (Graphics2D) g;
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
// Background grid and labels
|
||||
g2.setFont(new Font("Segoe UI", Font.PLAIN, 10));
|
||||
long maxMemRaw = Runtime.getRuntime().maxMemory();
|
||||
|
||||
for(int i = 0; i <= 4; i++) {
|
||||
int y = i == 0 ? 15 : height * i / 4;
|
||||
if (i == 4) y = height - 5;
|
||||
|
||||
g2.setColor(COLOR_BORDER);
|
||||
g2.drawLine(0, y, width, y);
|
||||
|
||||
// Draw Y-axis numbers
|
||||
g2.setColor(COLOR_TEXT_MUTED);
|
||||
long labelVal = (long) (maxMemRaw * (1.0 - (double)i / 4.0)) / 1024 / 1024;
|
||||
g2.drawString(labelVal + " MB", 5, y - 5);
|
||||
}
|
||||
|
||||
if (history.size() < 2) return;
|
||||
|
||||
double dx = (double) width / (MAX_POINTS - 1);
|
||||
Path2D path = new Path2D.Double();
|
||||
path.moveTo(0, height);
|
||||
|
||||
int i = MAX_POINTS - history.size();
|
||||
for (Double val : history) {
|
||||
double x = i * dx;
|
||||
double y = height - (val * height);
|
||||
if (i == MAX_POINTS - history.size()) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// Draw line
|
||||
g2.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||
g2.setColor(COLOR_PRIMARY);
|
||||
g2.draw(path);
|
||||
|
||||
// Fill area
|
||||
path.lineTo(width, height);
|
||||
path.lineTo((MAX_POINTS - history.size()) * dx, height);
|
||||
path.closePath();
|
||||
|
||||
GradientPaint fillPaint = new GradientPaint(
|
||||
0, 0, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 120),
|
||||
0, height, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 10)
|
||||
);
|
||||
g2.setPaint(fillPaint);
|
||||
g2.fill(path);
|
||||
|
||||
Double lastValue = history.peekLast();
|
||||
if (lastValue != null) {
|
||||
String usageLabel = String.format("Usage %.1f%%", lastValue * 100.0);
|
||||
g2.setFont(new Font("Segoe UI", Font.BOLD, 12));
|
||||
FontMetrics metrics = g2.getFontMetrics();
|
||||
int labelWidth = metrics.stringWidth(usageLabel) + 16;
|
||||
int labelHeight = 24;
|
||||
int labelX = Math.max(8, width - labelWidth - 8);
|
||||
int labelY = 8;
|
||||
|
||||
g2.setColor(new Color(0, 0, 0, 130));
|
||||
g2.fillRoundRect(labelX, labelY, labelWidth, labelHeight, 12, 12);
|
||||
g2.setColor(COLOR_TEXT);
|
||||
g2.drawString(usageLabel, labelX + 8, labelY + 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatDuration(long millis) {
|
||||
long totalSeconds = Math.max(0L, millis / 1000L);
|
||||
long hours = totalSeconds / 3600L;
|
||||
long minutes = (totalSeconds % 3600L) / 60L;
|
||||
long seconds = totalSeconds % 60L;
|
||||
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,15 @@ import com.eu.habbo.habbohotel.achievements.AchievementManager;
|
||||
import com.eu.habbo.habbohotel.bots.BotManager;
|
||||
import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
||||
import com.eu.habbo.habbohotel.wheel.WheelManager;
|
||||
import com.eu.habbo.habbohotel.soundboard.SoundboardManager;
|
||||
import com.eu.habbo.habbohotel.mentions.MentionManager;
|
||||
import com.eu.habbo.habbohotel.commands.CommandHandler;
|
||||
import com.eu.habbo.habbohotel.crafting.CraftingManager;
|
||||
import com.eu.habbo.habbohotel.guides.GuideManager;
|
||||
import com.eu.habbo.habbohotel.guilds.GuildManager;
|
||||
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.modtool.ModToolManager;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||
@@ -44,6 +48,7 @@ public class GameEnvironment {
|
||||
private NavigatorManager navigatorManager;
|
||||
private GuildManager guildManager;
|
||||
private ItemManager itemManager;
|
||||
private FurnitureTextProvider furnitureTextProvider;
|
||||
private CatalogManager catalogManager;
|
||||
private HotelViewManager hotelViewManager;
|
||||
private RoomManager roomManager;
|
||||
@@ -64,6 +69,9 @@ public class GameEnvironment {
|
||||
private GoogleTranslateManager googleTranslateManager;
|
||||
private CustomBadgeManager customBadgeManager;
|
||||
private InfostandBackgroundManager infostandBackgroundManager;
|
||||
private WheelManager wheelManager;
|
||||
private SoundboardManager soundboardManager;
|
||||
private MentionManager mentionManager;
|
||||
|
||||
public void load() throws Exception {
|
||||
LOGGER.info("GameEnvironment -> Loading...");
|
||||
@@ -73,6 +81,8 @@ public class GameEnvironment {
|
||||
this.hotelViewManager = new HotelViewManager();
|
||||
this.itemManager = new ItemManager();
|
||||
this.itemManager.load();
|
||||
this.furnitureTextProvider = new FurnitureTextProvider();
|
||||
this.furnitureTextProvider.init();
|
||||
this.botManager = new BotManager();
|
||||
this.petManager = new PetManager();
|
||||
this.guildManager = new GuildManager();
|
||||
@@ -93,6 +103,9 @@ public class GameEnvironment {
|
||||
this.googleTranslateManager = new GoogleTranslateManager();
|
||||
this.customBadgeManager = new CustomBadgeManager();
|
||||
this.infostandBackgroundManager = new InfostandBackgroundManager();
|
||||
this.wheelManager = new WheelManager();
|
||||
this.soundboardManager = new SoundboardManager();
|
||||
this.mentionManager = new MentionManager();
|
||||
|
||||
this.roomManager.loadPublicRooms();
|
||||
this.navigatorManager.loadNavigator();
|
||||
@@ -152,10 +165,22 @@ public class GameEnvironment {
|
||||
return this.itemManager;
|
||||
}
|
||||
|
||||
public FurnitureTextProvider getFurnitureTextProvider() {
|
||||
return this.furnitureTextProvider;
|
||||
}
|
||||
|
||||
public CatalogManager getCatalogManager() {
|
||||
return this.catalogManager;
|
||||
}
|
||||
|
||||
public WheelManager getWheelManager() {
|
||||
return this.wheelManager;
|
||||
}
|
||||
|
||||
public SoundboardManager getSoundboardManager() {
|
||||
return this.soundboardManager;
|
||||
}
|
||||
|
||||
public HotelViewManager getHotelViewManager() {
|
||||
return this.hotelViewManager;
|
||||
}
|
||||
@@ -188,6 +213,10 @@ public class GameEnvironment {
|
||||
return this.petManager;
|
||||
}
|
||||
|
||||
public MentionManager getMentionManager() {
|
||||
return this.mentionManager;
|
||||
}
|
||||
|
||||
public AchievementManager getAchievementManager() {
|
||||
return this.achievementManager;
|
||||
}
|
||||
|
||||
@@ -100,9 +100,9 @@ public class AchievementManager {
|
||||
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
|
||||
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) {
|
||||
for (TalentTrackType type : TalentTrackType.values()) {
|
||||
|
||||
@@ -138,18 +138,20 @@ public class Bot implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.needsUpdate) {
|
||||
Room localRoom = this.room;
|
||||
RoomUnit localRoomUnit = this.roomUnit;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE bots SET name = ?, motto = ?, figure = ?, gender = ?, user_id = ?, room_id = ?, x = ?, y = ?, z = ?, rot = ?, dance = ?, freeroam = ?, chat_lines = ?, chat_auto = ?, chat_random = ?, chat_delay = ?, effect = ?, bubble_id = ? WHERE id = ?")) {
|
||||
statement.setString(1, this.name);
|
||||
statement.setString(2, this.motto);
|
||||
statement.setString(3, this.figure);
|
||||
statement.setString(4, this.gender.toString());
|
||||
statement.setInt(5, this.ownerId);
|
||||
statement.setInt(6, this.room == null ? 0 : this.room.getId());
|
||||
statement.setInt(7, this.roomUnit == null ? 0 : this.roomUnit.getX());
|
||||
statement.setInt(8, this.roomUnit == null ? 0 : this.roomUnit.getY());
|
||||
statement.setDouble(9, this.roomUnit == null ? 0 : this.roomUnit.getZ());
|
||||
statement.setInt(10, this.roomUnit == null ? 0 : this.roomUnit.getBodyRotation().getValue());
|
||||
statement.setInt(11, this.roomUnit == null ? 0 : this.roomUnit.getDanceType().getType());
|
||||
statement.setInt(6, localRoom == null ? 0 : localRoom.getId());
|
||||
statement.setInt(7, localRoomUnit == null ? 0 : localRoomUnit.getX());
|
||||
statement.setInt(8, localRoomUnit == null ? 0 : localRoomUnit.getY());
|
||||
statement.setDouble(9, localRoomUnit == null ? 0 : localRoomUnit.getZ());
|
||||
statement.setInt(10, localRoomUnit == null ? 0 : localRoomUnit.getBodyRotation().getValue());
|
||||
statement.setInt(11, localRoomUnit == null ? 0 : localRoomUnit.getDanceType().getType());
|
||||
statement.setString(12, this.canWalk ? "1" : "0");
|
||||
StringBuilder text = new StringBuilder();
|
||||
for (String s : this.chatLines) {
|
||||
@@ -187,11 +189,7 @@ public class Bot implements Runnable {
|
||||
int timeOut = Emulator.getRandom().nextInt(20) * 2;
|
||||
this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp());
|
||||
}
|
||||
}/* else {
|
||||
for (RoomTile t : this.room.getLayout().getTilesAround(this.room.getLayout().getTile(this.getRoomUnit().getX(), this.getRoomUnit().getY()))) {
|
||||
WiredManager.handle(WiredTriggerType.BOT_REACHED_STF, this.roomUnit, this.room, this.room.getItemsAt(t).toArray());
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) {
|
||||
@@ -216,7 +214,7 @@ public class Bot implements Runnable {
|
||||
} else {
|
||||
this.lastChatIndex++;
|
||||
if (this.lastChatIndex >= this.chatLines.size()) {
|
||||
this.lastChatIndex = 0; // start from scratch :-3
|
||||
this.lastChatIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +280,7 @@ public class Bot implements Runnable {
|
||||
}
|
||||
|
||||
public void onPickUp(Habbo habbo, Room room) {
|
||||
|
||||
this.stopFollowingHabbo();
|
||||
}
|
||||
|
||||
public void onUserSay(final RoomChatMessage message) {
|
||||
@@ -308,9 +306,6 @@ public class Bot implements Runnable {
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
this.needsUpdate = true;
|
||||
|
||||
//if(this.room != null)
|
||||
//this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose());
|
||||
}
|
||||
|
||||
public String getMotto() {
|
||||
@@ -537,5 +532,28 @@ public class Bot implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11};
|
||||
|
||||
public static final int ACTION_ROTATE = 11;
|
||||
|
||||
private static final long MIN_OWNER_ACTION_INTERVAL_MS = 200L;
|
||||
|
||||
private volatile long lastOwnerActionAt;
|
||||
|
||||
public short[] getOwnerActionIds() {
|
||||
return DEFAULT_OWNER_ACTION_IDS;
|
||||
}
|
||||
|
||||
public synchronized boolean tryAcquireOwnerActionSlot() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - this.lastOwnerActionAt < MIN_OWNER_ACTION_INTERVAL_MS) {
|
||||
return false;
|
||||
}
|
||||
this.lastOwnerActionAt = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onPostOwnerAction(int actionId) {
|
||||
// no-op default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ public class BotManager {
|
||||
addBotDefinition("generic", Bot.class);
|
||||
addBotDefinition("bartender", ButlerBot.class);
|
||||
addBotDefinition("visitor_log", VisitorBot.class);
|
||||
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
|
||||
|
||||
this.reload();
|
||||
|
||||
@@ -187,7 +188,11 @@ public class BotManager {
|
||||
if (pickedUpEvent.isCancelled())
|
||||
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) {
|
||||
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||
return;
|
||||
|
||||
@@ -20,10 +20,13 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ButlerBot extends Bot {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class);
|
||||
public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>();
|
||||
private static final ConcurrentHashMap<Pattern, Integer> serveItemsCompiled = new ConcurrentHashMap<>();
|
||||
|
||||
public ButlerBot(ResultSet set) throws SQLException {
|
||||
super(set);
|
||||
@@ -38,6 +41,7 @@ public class ButlerBot extends Bot {
|
||||
serveItems = new THashMap<>();
|
||||
|
||||
serveItems.clear();
|
||||
serveItemsCompiled.clear();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) {
|
||||
while (set.next()) {
|
||||
@@ -45,6 +49,17 @@ public class ButlerBot extends Bot {
|
||||
THashSet<String> ks = new THashSet<>();
|
||||
Collections.addAll(ks, keys);
|
||||
serveItems.put(ks, set.getInt("item"));
|
||||
|
||||
for (String key : keys) {
|
||||
if (key != null && !key.trim().isEmpty()) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(key.toLowerCase()) + "\\b");
|
||||
serveItemsCompiled.put(pattern, set.getInt("item"));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to compile butler bot keyword pattern: {}", key, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
@@ -53,6 +68,7 @@ public class ButlerBot extends Bot {
|
||||
|
||||
public static void dispose() {
|
||||
serveItems.clear();
|
||||
serveItemsCompiled.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,74 +82,73 @@ public class ButlerBot extends Bot {
|
||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) {
|
||||
|
||||
if (message.getUnfilteredMessage() != null) {
|
||||
for (Map.Entry<THashSet<String>, Integer> set : serveItems.entrySet()) {
|
||||
for (String keyword : set.getKey()) {
|
||||
String unfilteredLower = message.getUnfilteredMessage().toLowerCase();
|
||||
for (Map.Entry<Pattern, Integer> entry : serveItemsCompiled.entrySet()) {
|
||||
Pattern pattern = entry.getKey();
|
||||
if (pattern.matcher(unfilteredLower).matches()) {
|
||||
int itemId = entry.getValue();
|
||||
String keyword = pattern.pattern().replace("\\b", "").replace("\\Q", "").replace("\\E", "");
|
||||
|
||||
// Check if the string contains a certain keyword using a regex.
|
||||
// If keyword = tea, teapot wouldn't trigger it.
|
||||
if (message.getUnfilteredMessage().toLowerCase().matches("\\b" + keyword + "\\b")) {
|
||||
|
||||
// Enable plugins to cancel this event
|
||||
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), set.getValue());
|
||||
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start give handitem process
|
||||
if (this.getRoomUnit().canWalk()) {
|
||||
final String key = keyword;
|
||||
final Bot bot = this;
|
||||
|
||||
// Step 1: Look at Habbo
|
||||
bot.lookAt(serveEvent.habbo);
|
||||
|
||||
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
|
||||
final List<Runnable> tasks = new ArrayList<>();
|
||||
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
|
||||
|
||||
tasks.add(() -> {
|
||||
if(this.getRoom() != null) {
|
||||
String botMessage = Emulator.getTexts()
|
||||
.getValue("bots.butler.given")
|
||||
.replace("%key%", key)
|
||||
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
|
||||
bot.talk(botMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
List<Runnable> failedReached = new ArrayList<>();
|
||||
failedReached.add(() -> {
|
||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
|
||||
for (Runnable task : tasks) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give bot the handitem that it's going to give the Habbo
|
||||
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
|
||||
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
|
||||
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
|
||||
} else {
|
||||
Emulator.getThreading().run(failedReached.get(0), 1000);
|
||||
}
|
||||
} else {
|
||||
if(this.getRoom() != null) {
|
||||
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
|
||||
|
||||
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
|
||||
this.talk(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Enable plugins to cancel this event
|
||||
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), itemId);
|
||||
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start give handitem process
|
||||
if (this.getRoomUnit().canWalk()) {
|
||||
final String key = keyword;
|
||||
final Bot bot = this;
|
||||
|
||||
// Step 1: Look at Habbo
|
||||
bot.lookAt(serveEvent.habbo);
|
||||
|
||||
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
|
||||
final List<Runnable> tasks = new ArrayList<>();
|
||||
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
|
||||
|
||||
tasks.add(() -> {
|
||||
if(this.getRoom() != null) {
|
||||
String botMessage = Emulator.getTexts()
|
||||
.getValue("bots.butler.given")
|
||||
.replace("%key%", key)
|
||||
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
|
||||
bot.talk(botMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
List<Runnable> failedReached = new ArrayList<>();
|
||||
failedReached.add(() -> {
|
||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
|
||||
for (Runnable task : tasks) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give bot the handitem that it's going to give the Habbo
|
||||
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
|
||||
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
|
||||
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
|
||||
} else {
|
||||
Emulator.getThreading().run(failedReached.get(0), 1000);
|
||||
}
|
||||
} else {
|
||||
if(this.getRoom() != null) {
|
||||
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
|
||||
|
||||
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
|
||||
this.talk(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
package com.eu.habbo.habbohotel.bots;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.*;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer;
|
||||
import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FrankBot extends ButlerBot {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
|
||||
public static final String BOT_TYPE = "frank";
|
||||
public static final String PERMISSION_USE = "acc_bot_frank";
|
||||
private static final String KEY_DOOR_LINES = "__door_lines";
|
||||
private static final String KEY_BUSY_WHISPER = "__busy_whisper";
|
||||
private static final String KEY_DOOR_TRIGGERS = "__door_triggers";
|
||||
private static final List<String> DEFAULT_DOOR_LINES = List.of(
|
||||
"Right this way - mind the step!",
|
||||
"And out you go. Come back soon!",
|
||||
"Allow me to escort you to the exit.",
|
||||
"There's the door. Farewell, true believer!"
|
||||
);
|
||||
private static final String DEFAULT_BUSY_WHISPER =
|
||||
"Sorry, I am currently busy. Please wait until I am available.";
|
||||
private static final Pattern DEFAULT_DOOR_PATTERN = Pattern.compile(
|
||||
"\\b(show me the door|kick me|i want to leave|let me out)\\b");
|
||||
|
||||
private static final ConcurrentHashMap<Pattern, List<String>> chatResponses = new ConcurrentHashMap<>();
|
||||
private static volatile List<String> doorLines = DEFAULT_DOOR_LINES;
|
||||
private static volatile String busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
private static volatile Pattern doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final int MAX_CHAT_KEYWORDS = 256;
|
||||
private static final int MAX_DOOR_TRIGGERS = 32;
|
||||
private static final int MAX_MESSAGE_LEN = 256;
|
||||
private static final long BUSY_WHISPER_COOLDOWN_MS = 5000L;
|
||||
|
||||
private volatile RoomTile homeTile;
|
||||
private volatile RoomUserRotation homeRotation;
|
||||
private final AtomicBoolean busy = new AtomicBoolean(false);
|
||||
private final AtomicBoolean returnScheduled = new AtomicBoolean(false);
|
||||
private final ConcurrentHashMap<Integer, Long> lastBusyWhisperAt = new ConcurrentHashMap<>();
|
||||
|
||||
public FrankBot(ResultSet set) throws SQLException {
|
||||
super(set);
|
||||
}
|
||||
|
||||
public FrankBot(Bot bot) {
|
||||
super(bot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlace(Habbo habbo, Room room) {
|
||||
super.onPlace(habbo, room);
|
||||
if (this.getRoomUnit() != null) {
|
||||
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
}
|
||||
|
||||
private static final short[] FRANK_OWNER_ACTIONS = { (short) Bot.ACTION_ROTATE };
|
||||
|
||||
@Override
|
||||
public short[] getOwnerActionIds() {
|
||||
return FRANK_OWNER_ACTIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostOwnerAction(int actionId) {
|
||||
if (actionId == ACTION_ROTATE && this.getRoomUnit() != null) {
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialise() {
|
||||
chatResponses.clear();
|
||||
doorLines = DEFAULT_DOOR_LINES;
|
||||
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
Statement statement = connection.createStatement();
|
||||
ResultSet set = statement.executeQuery("SELECT `keys`, `responses` FROM bot_chat_responses WHERE bot_type = '" + BOT_TYPE + "'")) {
|
||||
while (set.next()) {
|
||||
String keysRaw = set.getString("keys");
|
||||
String responsesRaw = set.getString("responses");
|
||||
|
||||
if (keysRaw == null || responsesRaw == null) continue;
|
||||
|
||||
List<String> responses = new ArrayList<>();
|
||||
for (String line : responsesRaw.split("\n")) {
|
||||
String trimmed = line.trim();
|
||||
if (!trimmed.isEmpty()) responses.add(trimmed);
|
||||
}
|
||||
|
||||
if (responses.isEmpty()) continue;
|
||||
|
||||
String firstKey = keysRaw.split(";", 2)[0].trim();
|
||||
if (firstKey.startsWith("__")) {
|
||||
switch (firstKey) {
|
||||
case KEY_DOOR_LINES:
|
||||
doorLines = new CopyOnWriteArrayList<>(responses);
|
||||
break;
|
||||
case KEY_BUSY_WHISPER:
|
||||
busyWhisper = responses.get(0);
|
||||
break;
|
||||
case KEY_DOOR_TRIGGERS:
|
||||
doorTriggerPattern = buildDoorTriggerPattern(responses);
|
||||
break;
|
||||
default:
|
||||
LOGGER.warn("FrankBot: unknown system key '{}', ignored", firstKey);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> shared = new CopyOnWriteArrayList<>(responses);
|
||||
|
||||
for (String key : keysRaw.split(";")) {
|
||||
if (chatResponses.size() >= MAX_CHAT_KEYWORDS) {
|
||||
LOGGER.warn("FrankBot: chat keyword cap ({}) reached, remaining rows ignored",
|
||||
MAX_CHAT_KEYWORDS);
|
||||
break;
|
||||
}
|
||||
String k = key == null ? "" : key.trim().toLowerCase();
|
||||
if (k.isEmpty()) continue;
|
||||
try {
|
||||
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(k) + "\\b");
|
||||
chatResponses.put(pattern, shared);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to compile Frank chat keyword pattern: {}", k, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.warn("FrankBot: could not load bot_chat_responses ({}). Frank will still serve items.", e.getMessage());
|
||||
}
|
||||
|
||||
ButlerBot.initialise();
|
||||
}
|
||||
|
||||
public static void dispose() {
|
||||
chatResponses.clear();
|
||||
doorLines = DEFAULT_DOOR_LINES;
|
||||
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
ButlerBot.dispose();
|
||||
}
|
||||
|
||||
private static Pattern buildDoorTriggerPattern(List<String> triggers) {
|
||||
StringBuilder sb = new StringBuilder("\\b(");
|
||||
boolean first = true;
|
||||
int count = 0;
|
||||
for (String trigger : triggers) {
|
||||
if (count >= MAX_DOOR_TRIGGERS) {
|
||||
LOGGER.warn("FrankBot: door trigger cap ({}) reached, extra entries ignored",
|
||||
MAX_DOOR_TRIGGERS);
|
||||
break;
|
||||
}
|
||||
String t = trigger == null ? "" : trigger.trim().toLowerCase();
|
||||
if (t.isEmpty()) continue;
|
||||
if (!first) sb.append('|');
|
||||
sb.append(Pattern.quote(t));
|
||||
first = false;
|
||||
count++;
|
||||
}
|
||||
sb.append(")\\b");
|
||||
|
||||
if (first) return DEFAULT_DOOR_PATTERN;
|
||||
|
||||
try {
|
||||
return Pattern.compile(sb.toString());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("FrankBot: failed to compile door trigger pattern from {}, falling back to default", triggers, e);
|
||||
return DEFAULT_DOOR_PATTERN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserSay(final RoomChatMessage message) {
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null) return;
|
||||
|
||||
Habbo asker = message.getHabbo();
|
||||
if (asker == null || asker.getClient() == null) return;
|
||||
|
||||
if (this.getRoomUnit() == null) return;
|
||||
|
||||
String raw = message.getUnfilteredMessage();
|
||||
if (raw != null && raw.length() > MAX_MESSAGE_LEN) return;
|
||||
|
||||
if (this.homeTile == null) {
|
||||
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
|
||||
if (this.busy.get() || this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw != null) {
|
||||
double distance = this.getRoomUnit().getCurrentLocation().distance(asker.getRoomUnit().getCurrentLocation());
|
||||
int commandDistance = Emulator.getConfig().getInt("hotel.bot.butler.commanddistance");
|
||||
|
||||
if (distance <= commandDistance) {
|
||||
String lower = raw.toLowerCase();
|
||||
|
||||
if (doorTriggerPattern.matcher(lower).find()) {
|
||||
if (!this.busy.compareAndSet(false, true)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
this.showToTheDoor(asker);
|
||||
return;
|
||||
}
|
||||
|
||||
for (java.util.Map.Entry<Pattern, List<String>> entry : chatResponses.entrySet()) {
|
||||
if (entry.getKey().matcher(lower).find()) {
|
||||
List<String> options = entry.getValue();
|
||||
if (options.isEmpty()) continue;
|
||||
|
||||
String reply = options.get(RANDOM.nextInt(options.size()));
|
||||
this.talk(reply);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.busy.compareAndSet(false, true)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
super.onUserSay(message);
|
||||
this.schedulePostServeReturn(currentRoom.getId(), 0);
|
||||
}
|
||||
|
||||
private void whisperThrottled(Habbo target, String text) {
|
||||
if (target == null || text == null || text.isEmpty() || this.getRoomUnit() == null) return;
|
||||
int userId = target.getHabboInfo().getId();
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastBusyWhisperAt.get(userId);
|
||||
if (last != null && (now - last) < BUSY_WHISPER_COOLDOWN_MS) return;
|
||||
lastBusyWhisperAt.put(userId, now);
|
||||
RoomChatMessage msg = new RoomChatMessage(text, this.getRoomUnit(), RoomChatMessageBubbles.BOT);
|
||||
target.getClient().sendResponse(new RoomUserWhisperComposer(msg));
|
||||
}
|
||||
|
||||
private void showToTheDoor(final Habbo target) {
|
||||
final Room room = this.getRoom();
|
||||
if (room == null || room.getLayout() == null || target == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final RoomTile doorTile = room.getLayout().getDoorTile();
|
||||
if (doorTile == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lookAt(target);
|
||||
List<String> lines = doorLines;
|
||||
String line = lines.isEmpty() ? DEFAULT_DOOR_LINES.get(RANDOM.nextInt(DEFAULT_DOOR_LINES.size()))
|
||||
: lines.get(RANDOM.nextInt(lines.size()));
|
||||
this.talk(line);
|
||||
|
||||
final int targetId = target.getHabboInfo().getId();
|
||||
final int roomId = room.getId();
|
||||
final AtomicBoolean fired = new AtomicBoolean(false);
|
||||
|
||||
final Runnable kickThenReturn = () -> {
|
||||
if (!fired.compareAndSet(false, true)) return;
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null || currentRoom.getId() != roomId) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
Habbo stillHere = currentRoom.getHabbo(targetId);
|
||||
if (stillHere != null) {
|
||||
currentRoom.kickHabbo(stillHere, false);
|
||||
}
|
||||
this.scheduleReturnHome(targetId, roomId, 0);
|
||||
};
|
||||
|
||||
if (this.getRoomUnit().canWalk() && !this.getRoomUnit().getCurrentLocation().equals(doorTile)) {
|
||||
List<Runnable> onArrive = new ArrayList<>();
|
||||
onArrive.add(kickThenReturn);
|
||||
|
||||
List<Runnable> onFail = new ArrayList<>();
|
||||
onFail.add(() -> Emulator.getThreading().run(kickThenReturn, 1500));
|
||||
|
||||
this.getRoomUnit().setGoalLocation(doorTile);
|
||||
Emulator.getThreading().run(
|
||||
new RoomUnitWalkToLocation(this.getRoomUnit(), doorTile, room, onArrive, onFail));
|
||||
} else {
|
||||
Emulator.getThreading().run(kickThenReturn, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int RETURN_HOME_POLL_MS = 500;
|
||||
private static final int RETURN_HOME_MAX_WAIT_MS = 8000;
|
||||
private static final int POST_SERVE_POLL_MS = 750;
|
||||
private static final int POST_SERVE_MAX_WAIT_MS = 30000;
|
||||
|
||||
private void schedulePostServeReturn(final int roomId, final int waitedMs) {
|
||||
if (waitedMs == 0 && !this.returnScheduled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (waitedMs >= POST_SERVE_MAX_WAIT_MS) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
if (this.homeTile == null) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(() -> {
|
||||
Room r = this.getRoom();
|
||||
if (r == null || r.getId() != roomId || this.getRoomUnit() == null || this.homeTile == null) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||
if (this.homeRotation != null && this.getRoomUnit().getBodyRotation() != this.homeRotation) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
this.persistPosition();
|
||||
} else {
|
||||
this.busy.set(false);
|
||||
}
|
||||
this.returnScheduled.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean stillWalking = this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)
|
||||
|| (this.getRoomUnit().getPath() != null && !this.getRoomUnit().getPath().isEmpty());
|
||||
|
||||
if (stillWalking) {
|
||||
this.schedulePostServeReturn(roomId, waitedMs + POST_SERVE_POLL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
this.returnScheduled.set(false);
|
||||
this.returnHome(-1, false);
|
||||
}, POST_SERVE_POLL_MS);
|
||||
}
|
||||
|
||||
private void scheduleReturnHome(final int kickedHabboId, final int roomId, final int waitedMs) {
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null || currentRoom.getId() != roomId) return;
|
||||
|
||||
boolean stillEscorting = currentRoom.getHabbo(kickedHabboId) != null;
|
||||
|
||||
if (!stillEscorting || waitedMs >= RETURN_HOME_MAX_WAIT_MS) {
|
||||
this.returnHome(kickedHabboId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(
|
||||
() -> this.scheduleReturnHome(kickedHabboId, roomId, waitedMs + RETURN_HOME_POLL_MS),
|
||||
RETURN_HOME_POLL_MS);
|
||||
}
|
||||
|
||||
private void returnHome(int kickedHabboId, boolean alwaysTeleport) {
|
||||
final Room room = this.getRoom();
|
||||
if (room == null || this.homeTile == null || this.getRoomUnit() == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final Runnable teleportHome = () -> {
|
||||
Room r = this.getRoom();
|
||||
if (r == null || this.getRoomUnit() == null) return;
|
||||
|
||||
double homeZ = r.getTopHeightAt(this.homeTile.x, this.homeTile.y);
|
||||
|
||||
this.getRoomUnit().stopWalking();
|
||||
this.getRoomUnit().setZ(homeZ);
|
||||
this.getRoomUnit().setLocation(this.homeTile);
|
||||
this.getRoomUnit().setPreviousLocationZ(homeZ);
|
||||
if (this.homeRotation != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
}
|
||||
this.getRoomUnit().statusUpdate(true);
|
||||
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
this.persistPosition();
|
||||
};
|
||||
|
||||
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||
if (this.homeRotation != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
room.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
}
|
||||
this.persistPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasOtherWatchers = false;
|
||||
for (Habbo h : room.getCurrentHabbos().values()) {
|
||||
if (h.getHabboInfo().getId() != kickedHabboId) {
|
||||
hasOtherWatchers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alwaysTeleport || !hasOtherWatchers || !this.getRoomUnit().canWalk()) {
|
||||
teleportHome.run();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Runnable> onArrive = new ArrayList<>();
|
||||
onArrive.add(() -> {
|
||||
if (this.homeRotation != null && this.getRoom() != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
this.getRoom().sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
}
|
||||
this.persistPosition();
|
||||
});
|
||||
|
||||
List<Runnable> onFail = new ArrayList<>();
|
||||
onFail.add(teleportHome);
|
||||
|
||||
this.getRoomUnit().setGoalLocation(this.homeTile);
|
||||
Emulator.getThreading().run(
|
||||
new RoomUnitWalkToLocation(this.getRoomUnit(), this.homeTile, room, onArrive, onFail));
|
||||
}
|
||||
|
||||
private void persistPosition() {
|
||||
this.needsUpdate(true);
|
||||
this.run();
|
||||
this.busy.set(false);
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,8 @@ public class CatalogManager {
|
||||
public final Item ecotronItem;
|
||||
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
|
||||
private final List<Voucher> vouchers;
|
||||
public final TIntObjectMap<int[]> furnitureValues;
|
||||
private volatile byte[] rareValuesPayloadCache;
|
||||
|
||||
public CatalogManager() {
|
||||
long millis = System.currentTimeMillis();
|
||||
@@ -219,6 +221,7 @@ public class CatalogManager {
|
||||
this.buildersClubOfferDefs = new TIntIntHashMap();
|
||||
this.vouchers = new ArrayList<>();
|
||||
this.limitedNumbers = new THashMap<>();
|
||||
this.furnitureValues = new TIntObjectHashMap<>();
|
||||
|
||||
this.initialize();
|
||||
|
||||
@@ -243,6 +246,76 @@ public class CatalogManager {
|
||||
this.loadClothing();
|
||||
this.loadRecycler();
|
||||
this.loadGiftWrappers();
|
||||
this.loadFurnitureValues();
|
||||
}
|
||||
|
||||
private synchronized void loadFurnitureValues() {
|
||||
this.furnitureValues.clear();
|
||||
final int diamondType = Emulator.getConfig().getInt("seasonal.currency.diamond", 5);
|
||||
|
||||
for (CatalogPage page : this.catalogPages.valueCollection()) {
|
||||
for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) {
|
||||
if (catalogItem.getAmount() != 1)
|
||||
continue;
|
||||
|
||||
int credits = catalogItem.getCredits();
|
||||
int points = catalogItem.getPoints();
|
||||
int pointsType = catalogItem.getPointsType();
|
||||
|
||||
if (points <= 0 || pointsType != diamondType)
|
||||
continue;
|
||||
|
||||
THashSet<Item> baseItems = catalogItem.getBaseItems();
|
||||
|
||||
if (baseItems.size() != 1)
|
||||
continue;
|
||||
|
||||
for (Item item : baseItems) {
|
||||
FurnitureType type = item.getType();
|
||||
|
||||
if (type != FurnitureType.FLOOR && type != FurnitureType.WALL)
|
||||
continue;
|
||||
|
||||
int spriteId = item.getSpriteId();
|
||||
|
||||
if (spriteId > 0 && !this.furnitureValues.containsKey(spriteId)) {
|
||||
this.furnitureValues.put(spriteId, new int[]{credits, points, pointsType});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.rebuildRareValuesPayloadCache();
|
||||
|
||||
LOGGER.info("Furniture Values -> Loaded! ({} entries)", this.furnitureValues.size());
|
||||
}
|
||||
|
||||
private void rebuildRareValuesPayloadCache() {
|
||||
try (java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(this.furnitureValues.size() * 16 + 8);
|
||||
java.io.DataOutputStream out = new java.io.DataOutputStream(baos)) {
|
||||
out.writeInt(this.furnitureValues.size());
|
||||
TIntObjectIterator<int[]> iterator = this.furnitureValues.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.advance();
|
||||
int[] value = iterator.value();
|
||||
out.writeInt(iterator.key()); // spriteId
|
||||
out.writeInt(value[0]); // credits
|
||||
out.writeInt(value[1]); // points
|
||||
out.writeInt(value[2]); // pointsType
|
||||
}
|
||||
this.rareValuesPayloadCache = baos.toByteArray();
|
||||
} catch (java.io.IOException e) {
|
||||
LOGGER.error("Failed to build rare values payload cache", e);
|
||||
this.rareValuesPayloadCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
public TIntObjectMap<int[]> getFurnitureValues() {
|
||||
return this.furnitureValues;
|
||||
}
|
||||
|
||||
public byte[] getRareValuesPayloadSnapshot() {
|
||||
return this.rareValuesPayloadCache;
|
||||
}
|
||||
|
||||
private synchronized void loadLimitedNumbers() {
|
||||
@@ -981,13 +1054,13 @@ public class CatalogManager {
|
||||
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
||||
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
||||
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;
|
||||
}
|
||||
|
||||
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1046,10 +1119,19 @@ public class CatalogManager {
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
|
||||
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
|
||||
String baseName = baseItem.getName();
|
||||
String type = item.getName().replace("rentable_bot_", "");
|
||||
type = type.replace("bot_", "");
|
||||
type = type.replace("visitor_logger", "visitor_log");
|
||||
|
||||
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|
||||
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
|
||||
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
|
||||
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
THashMap<String, String> data = new THashMap<>();
|
||||
|
||||
for (String s : item.getExtradata().split(";")) {
|
||||
@@ -1165,6 +1247,11 @@ public class CatalogManager {
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
|
||||
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);
|
||||
habboItem.setExtradata("");
|
||||
habboItem.needsUpdate(true);
|
||||
|
||||
@@ -72,7 +72,11 @@ public class ClubOffer implements ISerialize {
|
||||
this.type = OfferType.fromDatabase(set.getString("type"));
|
||||
this.vip = this.type == OfferType.VIP;
|
||||
this.deal = set.getString("deal").equals("1");
|
||||
this.giftable = set.getString("giftable").equals("1");
|
||||
boolean giftable = false;
|
||||
try {
|
||||
giftable = "1".equals(set.getString("giftable"));
|
||||
} catch (SQLException ignored) {}
|
||||
this.giftable = giftable;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
|
||||
+7
-3
@@ -42,14 +42,18 @@ public class RoomBundleLayout extends SingleBundle {
|
||||
}
|
||||
|
||||
if (this.room == null) {
|
||||
if (this.roomId > 0) {
|
||||
this.room = Emulator.getGameEnvironment().getRoomManager().loadRoom(this.roomId);
|
||||
RoomManager roomManager = Emulator.getGameEnvironment().getRoomManager();
|
||||
if (this.roomId > 0 && roomManager != null) {
|
||||
this.room = roomManager.loadRoom(this.roomId);
|
||||
|
||||
if (this.room != null)
|
||||
this.room.preventUnloading = true;
|
||||
} else {
|
||||
} else if (this.roomId <= 0) {
|
||||
LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId());
|
||||
}
|
||||
// roomManager can be null when CatalogManager.loadFurnitureValues() runs
|
||||
// during GameEnvironment.load() before RoomManager is constructed; in that
|
||||
// case skip eager room loading — the bundle resolves lazily at runtime.
|
||||
}
|
||||
|
||||
if (this.room == null) {
|
||||
|
||||
+15
-4
@@ -171,8 +171,9 @@ public class MarketPlace {
|
||||
statement.setInt(paramIndex++, maxPrice);
|
||||
}
|
||||
if (!search.isEmpty()) {
|
||||
statement.setString(paramIndex++, "%" + search + "%");
|
||||
statement.setString(paramIndex++, "%" + search + "%");
|
||||
String likeSearch = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(search) + "%";
|
||||
statement.setString(paramIndex++, likeSearch);
|
||||
statement.setString(paramIndex++, likeSearch);
|
||||
}
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
@@ -278,8 +279,9 @@ public class MarketPlace {
|
||||
return;
|
||||
}
|
||||
|
||||
int soldTimestamp = Emulator.getIntUnixTimestamp();
|
||||
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);
|
||||
int updated = updateOffer.executeUpdate();
|
||||
if (updated == 0) {
|
||||
@@ -306,7 +308,11 @@ public class MarketPlace {
|
||||
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,6 +374,11 @@ public class MarketPlace {
|
||||
event.item.setFromGift(false);
|
||||
|
||||
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().getItemsComponent().removeHabboItem(event.item);
|
||||
item.setUserId(-1);
|
||||
|
||||
+4
@@ -98,6 +98,10 @@ public class MarketPlaceOffer implements Runnable {
|
||||
return this.offerId;
|
||||
}
|
||||
|
||||
public boolean isPersisted() {
|
||||
return this.offerId > 0;
|
||||
}
|
||||
|
||||
public void setOfferId(int offerId) {
|
||||
this.offerId = offerId;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BanCommand extends Command {
|
||||
public BanCommand() {
|
||||
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
|
||||
@@ -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);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -191,11 +191,14 @@ public class CommandHandler {
|
||||
addCommand(new CreditsCommand());
|
||||
addCommand(new DanceCommand());
|
||||
addCommand(new DiagonalCommand());
|
||||
addCommand(new DisableMassMentionsCommand());
|
||||
addCommand(new DisableMentionsCommand());
|
||||
addCommand(new DisconnectCommand());
|
||||
addCommand(new EjectAllCommand());
|
||||
addCommand(new EmptyInventoryCommand());
|
||||
addCommand(new EmptyBotsInventoryCommand());
|
||||
addCommand(new EmptyPetsInventoryCommand());
|
||||
addCommand(new EmuStatsCommand());
|
||||
addCommand(new EnableCommand());
|
||||
addCommand(new EventCommand());
|
||||
addCommand(new FacelessCommand());
|
||||
@@ -300,7 +303,6 @@ public class CommandHandler {
|
||||
addCommand(new GivePrefixCommand());
|
||||
addCommand(new ListPrefixesCommand());
|
||||
addCommand(new RemovePrefixCommand());
|
||||
addCommand(new PrefixBlacklistCommand());
|
||||
addCommand(new WiredCommand());
|
||||
addCommand(new TestCommand());
|
||||
}
|
||||
|
||||
@@ -17,7 +17,22 @@ public class CommandsCommand extends Command {
|
||||
message.append("(").append(commands.size()).append("):\r\n");
|
||||
|
||||
for (Command c : commands) {
|
||||
message.append(Emulator.getTexts().getValue("commands.description." + c.permission, "commands.description." + c.permission)).append("\r");
|
||||
String textKey = "commands.description." + c.permission;
|
||||
String commandText = Emulator.getTexts().getValue(textKey, "");
|
||||
String commandLine = ":" + c.keys[0];
|
||||
String description = "";
|
||||
|
||||
if (commandText.startsWith(":")) {
|
||||
commandLine = commandText;
|
||||
} else if (!commandText.isEmpty() && !commandText.equals(textKey)) {
|
||||
description = commandText;
|
||||
}
|
||||
|
||||
message.append(commandLine).append("\r");
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
message.append(description).append("\r");
|
||||
}
|
||||
}
|
||||
|
||||
gameClient.getHabbo().alert(new String[]{message.toString()});
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
|
||||
public class DisableMassMentionsCommand extends Command {
|
||||
public DisableMassMentionsCommand() {
|
||||
super("cmd_disablemassmentions", new String[]{"disablemassmentions", "togglemassmentions"});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
if (gameClient == null) return true;
|
||||
Habbo habbo = gameClient.getHabbo();
|
||||
if (habbo == null || habbo.getHabboStats() == null) return true;
|
||||
|
||||
boolean newState = !habbo.getHabboStats().massMentionsEnabled();
|
||||
habbo.getHabboStats().setMassMentionsEnabled(newState);
|
||||
|
||||
habbo.whisper(newState
|
||||
? "Broadcast mentions (@all / @friends / @room) are now ENABLED for you."
|
||||
: "Broadcast mentions (@all / @friends / @room) are now DISABLED for you. Direct @nick mentions still work.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
|
||||
public class DisableMentionsCommand extends Command {
|
||||
public DisableMentionsCommand() {
|
||||
super("cmd_disablementions", new String[]{"disablementions", "togglementions"});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
if (gameClient == null) return true;
|
||||
Habbo habbo = gameClient.getHabbo();
|
||||
if (habbo == null || habbo.getHabboStats() == null) return true;
|
||||
|
||||
boolean newState = !habbo.getHabboStats().mentionsEnabled();
|
||||
habbo.getHabboStats().setMentionsEnabled(newState);
|
||||
|
||||
habbo.whisper(newState
|
||||
? "@mention notifications are now ENABLED for you."
|
||||
: "@mention notifications are now DISABLED for you. You will not receive direct or broadcast mentions.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
|
||||
public class EmuStatsCommand extends Command {
|
||||
public EmuStatsCommand() {
|
||||
super(Permission.ACC_MODTOOL_ROOM_INFO, new String[]{"emustats"});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) {
|
||||
gameClient.getHabbo().whisper("Emulator stats are available in the Nitro stats window.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.modtool.WordFilter;
|
||||
import com.eu.habbo.habbohotel.modtool.WordFilterWord;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -21,30 +22,44 @@ public class FilterWordCommand extends Command {
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
if (params.length < 2) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"));
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String word = params[1];
|
||||
|
||||
// Optional trailing "prefix" keyword marks the word as prefix-only (blocks
|
||||
// custom prefixes but not chat/motto/guild). Usage:
|
||||
// :filterword <word> -> everywhere, default replacement
|
||||
// :filterword <word> <replacement> -> everywhere
|
||||
// :filterword <word> prefix -> prefix-only, default replacement
|
||||
// :filterword <word> <replacement> prefix -> prefix-only
|
||||
boolean prefixOnly = false;
|
||||
String replacement = WordFilter.DEFAULT_REPLACEMENT;
|
||||
if (params.length == 3) {
|
||||
replacement = params[2];
|
||||
|
||||
if (params.length >= 3) {
|
||||
if (params[params.length - 1].equalsIgnoreCase("prefix")) {
|
||||
prefixOnly = true;
|
||||
if (params.length >= 4) replacement = params[2];
|
||||
} else {
|
||||
replacement = params[2];
|
||||
}
|
||||
}
|
||||
|
||||
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement);
|
||||
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement, prefixOnly);
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`) VALUES (?, ?)")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`, `prefix_only`) VALUES (?, ?, ?)")) {
|
||||
statement.setString(1, word);
|
||||
statement.setString(2, replacement);
|
||||
statement.setString(3, prefixOnly ? "1" : "0");
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"));
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement));
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement) + (prefixOnly ? " [prefix-only]" : ""), RoomChatMessageBubbles.ALERT);
|
||||
Emulator.getGameEnvironment().getWordFilter().addWord(wordFilterWord);
|
||||
|
||||
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.HabboManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IPBanCommand extends Command {
|
||||
public final static int TEN_YEARS = 315569260;
|
||||
|
||||
@@ -50,12 +52,12 @@ public class IPBanCommand extends Command {
|
||||
return true;
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||
count++;
|
||||
List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||
count += bans != null ? bans.size() : 0;
|
||||
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
|
||||
if (h != null) {
|
||||
count++;
|
||||
Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||
bans = Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||
count += bans != null ? bans.size() : 0;
|
||||
}
|
||||
}
|
||||
} 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.HabboManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MachineBanCommand extends Command {
|
||||
public MachineBanCommand() {
|
||||
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
|
||||
@@ -46,7 +48,8 @@ public class MachineBanCommand extends Command {
|
||||
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 {
|
||||
@@ -58,4 +61,4 @@ public class MachineBanCommand extends Command {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class PrefixBlacklistCommand extends Command {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PrefixBlacklistCommand.class);
|
||||
|
||||
public PrefixBlacklistCommand() {
|
||||
super("cmd_prefix_blacklist", Emulator.getTexts().getValue("commands.keys.cmd_prefix_blacklist").split(";"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
if (params.length < 2) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String action = params[1].toLowerCase();
|
||||
|
||||
if (action.equals("list")) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.header")).append("\r");
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist ORDER BY word")) {
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
int count = 0;
|
||||
while (set.next()) {
|
||||
sb.append("- ").append(set.getString("word")).append("\r");
|
||||
count++;
|
||||
}
|
||||
if (count == 0) {
|
||||
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.empty"));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error listing prefix blacklist", e);
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (params.length < 3) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String word = params[2].toLowerCase().trim();
|
||||
|
||||
if (word.isEmpty()) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.empty_word"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action.equals("add")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO custom_prefix_blacklist (word) VALUES (?)")) {
|
||||
statement.setString(1, word);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error adding prefix blacklist word", e);
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.added").replace("%word%", word),
|
||||
RoomChatMessageBubbles.ALERT
|
||||
);
|
||||
} else if (action.equals("remove")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM custom_prefix_blacklist WHERE word = ?")) {
|
||||
statement.setString(1, word);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error removing prefix blacklist word", e);
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.removed").replace("%word%", word),
|
||||
RoomChatMessageBubbles.ALERT
|
||||
);
|
||||
} else {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+40
-1
@@ -2,7 +2,12 @@ package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
|
||||
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
|
||||
|
||||
public class UpdatePermissionsCommand extends Command {
|
||||
public UpdatePermissionsCommand() {
|
||||
@@ -13,7 +18,41 @@ public class UpdatePermissionsCommand extends Command {
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
Emulator.getGameEnvironment().getPermissionsManager().reload();
|
||||
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_update_permissions"), RoomChatMessageBubbles.ALERT);
|
||||
// PermissionsManager.reload() rebuilt the rank table — each online
|
||||
// Habbo's HabboInfo still references the OLD Rank object, so
|
||||
// server-side hasPermission() / wire composers would keep
|
||||
// reporting stale data until relogin. Re-bind every connected
|
||||
// user to the freshly-loaded Rank by id, then ship the new
|
||||
// UserPermissionsComposer (which carries clubLevel,
|
||||
// securityLevel, isAmbassador, rank metadata and the resolved
|
||||
// permission_definitions map) so Nitro clients' React-side
|
||||
// useHasPermission(key) / useUserRank() / useUserPermissions()
|
||||
// consumers re-render against the updated tables without an F5.
|
||||
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
|
||||
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
|
||||
|
||||
int refreshed = 0;
|
||||
|
||||
for (Habbo habbo : habboManager.getOnlineHabbos().values()) {
|
||||
if (habbo == null || habbo.getHabboInfo() == null || habbo.getClient() == null) continue;
|
||||
|
||||
int currentRankId = habbo.getHabboInfo().getRank().getId();
|
||||
// Defensive fallback: if the admin deleted the rank from the
|
||||
// permission_ranks table between sessions, fall back to rank 1
|
||||
// (Member) so the user isn't stranded with a null Rank.
|
||||
Rank freshRank = permissions.rankExists(currentRankId)
|
||||
? permissions.getRank(currentRankId)
|
||||
: permissions.getRank(1);
|
||||
|
||||
habbo.getHabboInfo().setRank(freshRank);
|
||||
habbo.getClient().sendResponse(new UserPermissionsComposer(habbo));
|
||||
refreshed++;
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_update_permissions") + " (" + refreshed + " online refreshed)",
|
||||
RoomChatMessageBubbles.ALERT
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -149,13 +149,23 @@ public class GameClient {
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
this.dispose(true);
|
||||
}
|
||||
|
||||
public void dispose(boolean allowSessionResume) {
|
||||
try {
|
||||
this.channel.close();
|
||||
|
||||
if (this.habbo != null) {
|
||||
if (this.habbo.isOnline()) {
|
||||
// Agisci sull'Habbo SOLO se è ancora attaccato a QUESTO client. Su un
|
||||
// reconnect veloce (drop Cloudflare → il client riconnette) l'Habbo può
|
||||
// essere già stato riassegnato alla NUOVA connessione (session resume):
|
||||
// in quel caso questo dispose della vecchia connessione NON deve
|
||||
// parcheggiarlo né disconnetterlo, altrimenti ucciderebbe la sessione
|
||||
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
|
||||
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
|
||||
// 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) {
|
||||
// No grace period configured — immediate disconnect as before
|
||||
@@ -171,4 +181,4 @@ public class GameClient {
|
||||
LOGGER.error("Caught exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,14 +43,34 @@ public class GameClientManager {
|
||||
|
||||
|
||||
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) {
|
||||
this.disposeClient(channel, true);
|
||||
}
|
||||
|
||||
private void disposeClient(Channel channel, boolean allowSessionResume) {
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
|
||||
|
||||
if (client != null) {
|
||||
client.dispose();
|
||||
client.dispose(allowSessionResume);
|
||||
}
|
||||
channel.deregister();
|
||||
channel.attr(GameServerAttributes.CLIENT).set(null);
|
||||
@@ -190,4 +210,4 @@ public class GameClientManager {
|
||||
CFKeepAlive();
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-4
@@ -71,6 +71,15 @@ public class SessionResumeManager {
|
||||
}
|
||||
}, 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));
|
||||
|
||||
applyPausedEffect(habbo);
|
||||
@@ -118,16 +127,32 @@ public class SessionResumeManager {
|
||||
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
|
||||
}
|
||||
|
||||
clearSsoTicket(habbo.getHabboInfo().getId());
|
||||
// NON svuotare il ticket SSO qui. Dietro Cloudflare la pagina si ricarica
|
||||
// lentamente (~15s) e la grace (5s) scade prima che la nuova connessione
|
||||
// arrivi: svuotando il ticket si cancellava quello NUOVO appena scritto dal
|
||||
// CMS per il refresh → "non-existing SSO token" → bisognava refreshare 2 volte.
|
||||
// Il ticket vive col suo TTL (auth_ticket_expires_at) e viene sovrascritto dal
|
||||
// CMS al prossimo /client o azzerato al logout.
|
||||
}
|
||||
|
||||
private void restoreSsoTicket(int userId, String ssoTicket) {
|
||||
// Restore the old ticket ONLY if no fresh ticket has been written in the
|
||||
// meantime. On a hard-refresh the CMS writes a NEW auth_ticket for the same
|
||||
// user before this parking restore runs; without the guard we'd clobber it
|
||||
// with the old ticket, so the new connection's SSO wouldn't be found and the
|
||||
// client would get "session expired" on the first attempt. The guard means:
|
||||
// normal reconnect (ticket cleared to '' after login) -> restore; hard-refresh
|
||||
// (CMS already wrote a new ticket) -> leave the new ticket untouched.
|
||||
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
|
||||
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? AND (auth_ticket = '' OR auth_ticket IS NULL) LIMIT 1")) {
|
||||
statement.setString(1, ssoTicket);
|
||||
statement.setInt(2, userId);
|
||||
statement.execute();
|
||||
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
|
||||
int updated = statement.executeUpdate();
|
||||
if (updated > 0) {
|
||||
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
|
||||
} else {
|
||||
LOGGER.info("[SessionResume] Skipped SSO restore for user {} — a newer ticket is already present (likely a fresh login/hard-refresh)", userId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
|
||||
}
|
||||
|
||||
@@ -208,10 +208,10 @@ public abstract class Game implements Runnable {
|
||||
this.state = GameState.IDLE;
|
||||
|
||||
boolean gamesActive = false;
|
||||
for (HabboItem timer : room.getFloorItems()) {
|
||||
if (timer instanceof InteractionGameTimer) {
|
||||
if (((InteractionGameTimer) timer).isRunning())
|
||||
gamesActive = true;
|
||||
for (InteractionGameTimer timer : room.getRoomSpecialTypes().getGameTimers().values()) {
|
||||
if (timer.isRunning()) {
|
||||
gamesActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,49 +6,55 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
|
||||
public class GamePlayer {
|
||||
|
||||
private final Habbo habbo;
|
||||
|
||||
|
||||
private GameTeamColors teamColor;
|
||||
|
||||
|
||||
private int score;
|
||||
private int wiredScore;
|
||||
|
||||
|
||||
public GamePlayer(Habbo habbo, GameTeamColors teamColor) {
|
||||
this.habbo = habbo;
|
||||
this.teamColor = teamColor;
|
||||
}
|
||||
|
||||
|
||||
public void reset() {
|
||||
this.score = 0;
|
||||
this.wiredScore = 0;
|
||||
}
|
||||
|
||||
public synchronized void addScore(int amount) {
|
||||
public void addScore(int amount) {
|
||||
addScore(amount, false);
|
||||
}
|
||||
|
||||
public synchronized void addScore(int amount, boolean isWired) {
|
||||
if (habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
|
||||
this.score += amount;
|
||||
public void addScore(int amount, boolean isWired) {
|
||||
com.eu.habbo.habbohotel.rooms.Room roomToTrigger = null;
|
||||
com.eu.habbo.habbohotel.rooms.RoomUnit roomUnitToTrigger = null;
|
||||
int currentScore = 0;
|
||||
|
||||
if (this.score < 0) this.score = 0;
|
||||
synchronized (this) {
|
||||
if (this.habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
|
||||
this.score += amount;
|
||||
|
||||
if(isWired) {
|
||||
this.wiredScore += amount;
|
||||
if (this.score < 0) this.score = 0;
|
||||
|
||||
if (this.wiredScore < 0) {
|
||||
this.wiredScore = 0;
|
||||
if (isWired) {
|
||||
this.wiredScore += amount;
|
||||
|
||||
if (this.wiredScore < 0) {
|
||||
this.wiredScore = 0;
|
||||
}
|
||||
|
||||
if (this.wiredScore > this.score) {
|
||||
this.wiredScore = this.score;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.wiredScore > this.score) {
|
||||
this.wiredScore = this.score;
|
||||
}
|
||||
roomToTrigger = this.habbo.getHabboInfo().getCurrentRoom();
|
||||
roomUnitToTrigger = this.habbo.getRoomUnit();
|
||||
currentScore = this.score;
|
||||
}
|
||||
}
|
||||
|
||||
WiredManager.triggerScoreAchieved(this.habbo.getHabboInfo().getCurrentRoom(), this.habbo.getRoomUnit(), this.score, amount);
|
||||
if (roomToTrigger != null && roomUnitToTrigger != null) {
|
||||
WiredManager.triggerScoreAchieved(roomToTrigger, roomUnitToTrigger, currentScore, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +62,10 @@ public class GamePlayer {
|
||||
return this.habbo;
|
||||
}
|
||||
|
||||
|
||||
public GameTeamColors getTeamColor() {
|
||||
return this.teamColor;
|
||||
}
|
||||
|
||||
|
||||
public int getScore() {
|
||||
return this.score;
|
||||
}
|
||||
|
||||
@@ -252,6 +252,25 @@ public class Guild implements Runnable {
|
||||
return this.readForum;
|
||||
}
|
||||
|
||||
public boolean canHabboReadForum(int habboId, GuildMember member, boolean staff) {
|
||||
if (staff || this.getOwnerId() == habboId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (this.readForum) {
|
||||
case EVERYONE:
|
||||
return true;
|
||||
case MEMBERS:
|
||||
return member != null && member.getRank().type <= GuildRank.MEMBER.type;
|
||||
case ADMINS:
|
||||
return member != null && member.getRank().type < GuildRank.MEMBER.type;
|
||||
case OWNER:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setReadForum(SettingsState readForum) {
|
||||
this.readForum = readForum;
|
||||
}
|
||||
|
||||
@@ -421,9 +421,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 ?, ?")) {
|
||||
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(4, (page * 14) + 14);
|
||||
statement.setInt(4, 14);
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
|
||||
@@ -101,21 +101,27 @@ public class ForumThread implements Runnable, ISerialize {
|
||||
if (statement.executeUpdate() < 1)
|
||||
return null;
|
||||
|
||||
ResultSet set = statement.getGeneratedKeys();
|
||||
if (set.next()) {
|
||||
int threadId = set.getInt(1);
|
||||
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
||||
cacheThread(createdThread);
|
||||
|
||||
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
|
||||
createdThread.addComment(comment);
|
||||
|
||||
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
|
||||
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||
if (set.next()) {
|
||||
int threadId = set.getInt(1);
|
||||
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
||||
cacheThread(createdThread);
|
||||
}
|
||||
}
|
||||
} catch (SQLException 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;
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -98,12 +98,13 @@ public class ForumThreadComment implements Runnable, ISerialize {
|
||||
if (statement.executeUpdate() < 1)
|
||||
return null;
|
||||
|
||||
ResultSet set = statement.getGeneratedKeys();
|
||||
if (set.next()) {
|
||||
int commentId = set.getInt(1);
|
||||
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
|
||||
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||
if (set.next()) {
|
||||
int commentId = set.getInt(1);
|
||||
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) {
|
||||
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,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,172 @@
|
||||
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", "");
|
||||
if (!override.isEmpty()) {
|
||||
Path p = Paths.get(override);
|
||||
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path");
|
||||
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path does not exist");
|
||||
}
|
||||
|
||||
String rendererConfigPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
||||
String assetBasePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
|
||||
if (!rendererConfigPath.isEmpty()) {
|
||||
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
|
||||
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
|
||||
}
|
||||
|
||||
Source fallback = resolveFromAssetBase(assetBasePath);
|
||||
if (fallback != null) return fallback;
|
||||
|
||||
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
|
||||
} 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 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,272 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/** 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,181 @@
|
||||
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 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("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source);
|
||||
|
||||
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 static Path resolveSource() {
|
||||
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve();
|
||||
if (source.ok()) 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) {}
|
||||
}
|
||||
@@ -48,6 +48,12 @@ public class Item implements ISerialize {
|
||||
return item.getName().toLowerCase().startsWith("a0 pet");
|
||||
}
|
||||
|
||||
public static boolean isBot(Item item) {
|
||||
if (item == null) return false;
|
||||
String name = item.getName();
|
||||
return name != null && (name.startsWith("bot_") || name.startsWith("rentable_bot_"));
|
||||
}
|
||||
|
||||
public static double getCurrentHeight(HabboItem item) {
|
||||
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
|
||||
if (item.getExtradata().isEmpty()) {
|
||||
@@ -117,7 +123,7 @@ public class Item implements ISerialize {
|
||||
|
||||
if (!set.getString("vending_ids").isEmpty()) {
|
||||
this.vendingItems = new TIntArrayList();
|
||||
String[] vendingIds = set.getString("vending_ids").replace(";", ",").split(",");
|
||||
String[] vendingIds = set.getString("vending_ids").replace(";", ",").replace(".", ",").split(",");
|
||||
for (String s : vendingIds) {
|
||||
this.vendingItems.add(Integer.parseInt(s.replace(" ", "")));
|
||||
}
|
||||
@@ -161,6 +167,20 @@ public class Item implements ISerialize {
|
||||
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() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
+11
@@ -13,11 +13,13 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class InteractionGift extends HabboItem {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
|
||||
|
||||
public boolean explode = false;
|
||||
private final AtomicBoolean opening = new AtomicBoolean(false);
|
||||
private int[] itemId;
|
||||
private int colorId = 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
|
||||
public void serializeExtradata(ServerMessage serverMessage) {
|
||||
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
|
||||
|
||||
+2
-3
@@ -65,9 +65,8 @@ public class InteractionMultiHeight extends HabboItem {
|
||||
if (this.getBaseItem().getMultiHeights().length > 0) {
|
||||
this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length));
|
||||
this.needsUpdate(true);
|
||||
room.updateTiles(room.getLayout().getTilesAt(room.getLayout().getTile(this.getX(), this.getY()), this.getBaseItem().getWidth(), this.getBaseItem().getLength(), this.getRotation()));
|
||||
room.updateItemState(this);
|
||||
//room.sendComposer(new UpdateStackHeightComposer(this.getX(), this.getY(), this.getBaseItem().getMultiHeights()[Integer.valueOf(this.getExtradata())] * 256.0D).compose());
|
||||
room.updateItem(this);
|
||||
this.updateUnitsOnItem(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -89,7 +89,9 @@ public class InteractionOneWayGate extends HabboItem {
|
||||
Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onFail, onFail));
|
||||
|
||||
Emulator.getThreading().run(() -> {
|
||||
WiredManager.triggerUserWalksOn(room, unit, this);
|
||||
if (room.isLoaded()) {
|
||||
WiredManager.triggerUserWalksOn(room, unit, this);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
+4
@@ -29,6 +29,10 @@ public class InteractionRoomAds extends InteractionCustomValues {
|
||||
{
|
||||
this.put("offsetZ", "0");
|
||||
}
|
||||
|
||||
{
|
||||
this.put("scale", "100");
|
||||
}
|
||||
};
|
||||
|
||||
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
|
||||
|
||||
+38
-16
@@ -18,6 +18,7 @@ import java.sql.SQLException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
|
||||
@@ -61,7 +62,11 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
*/
|
||||
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<>();
|
||||
|
||||
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
|
||||
@@ -93,23 +98,24 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.needsUpdate()) {
|
||||
String wiredData = this.getWiredData();
|
||||
String wiredDataRaw = this.getWiredData();
|
||||
final String wiredData = (wiredDataRaw == null) ? "" : wiredDataRaw;
|
||||
final int currentRoomId = this.getRoomId();
|
||||
final int currentId = this.getId();
|
||||
|
||||
if (wiredData == null) {
|
||||
wiredData = "";
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
|
||||
if (this.getRoomId() != 0) {
|
||||
statement.setString(1, wiredData);
|
||||
} else {
|
||||
statement.setString(1, "");
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
|
||||
if (currentRoomId != 0) {
|
||||
statement.setString(1, wiredData);
|
||||
} else {
|
||||
statement.setString(1, "");
|
||||
}
|
||||
statement.setInt(2, currentId);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
statement.setInt(2, this.getId());
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
super.run();
|
||||
}
|
||||
@@ -148,6 +154,15 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
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
|
||||
public boolean allowWiredResetState() {
|
||||
return false;
|
||||
@@ -216,6 +231,9 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
|
||||
{
|
||||
int intParamCount = packet.readInt();
|
||||
if (intParamCount < 0 || intParamCount > 100) {
|
||||
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount);
|
||||
}
|
||||
int[] intParams = new int[intParamCount];
|
||||
|
||||
for(int i = 0; i < intParamCount; i++)
|
||||
@@ -226,6 +244,10 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
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++)
|
||||
|
||||
+20
-11
@@ -154,6 +154,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
@Override
|
||||
public void onPickUp(Room room) {
|
||||
this.endGame(room);
|
||||
this.threadActive = false;
|
||||
|
||||
this.timeNow = this.getInitialTimeValue();
|
||||
this.setExtradata(this.timeNow + "\t" + this.baseTime);
|
||||
@@ -220,8 +221,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
room.updateItem(this);
|
||||
WiredManager.triggerGameStarts(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||
}
|
||||
} else if (client != null) {
|
||||
@@ -243,8 +243,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
} else {
|
||||
this.unpause(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -257,8 +256,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
this.createNewGame(room);
|
||||
WiredManager.triggerGameStarts(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -297,8 +295,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
}
|
||||
this.createNewGame(room);
|
||||
WiredManager.triggerGameStarts(room);
|
||||
if (!threadActive) {
|
||||
threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -321,8 +318,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
this.isPaused = false;
|
||||
this.unpause(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -406,7 +402,9 @@ public class InteractionGameTimer extends HabboItem {
|
||||
}
|
||||
|
||||
public void setThreadActive(boolean threadActive) {
|
||||
this.threadActive = threadActive;
|
||||
synchronized (this) {
|
||||
this.threadActive = threadActive;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
@@ -428,4 +426,15 @@ public class InteractionGameTimer extends HabboItem {
|
||||
public int getBaseTime() {
|
||||
return this.baseTime;
|
||||
}
|
||||
|
||||
public boolean tryActivateTimerThread() {
|
||||
synchronized (this) {
|
||||
if (this.threadActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.threadActive = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -190,6 +190,14 @@ public class InteractionPetBreedingNest extends HabboItem {
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
this.setExtradata("2");
|
||||
|
||||
+8
-2
@@ -65,8 +65,14 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
|
||||
} else {
|
||||
String[] data = wiredData.split(":");
|
||||
|
||||
this.lowerLimit = Integer.parseInt(data[0]);
|
||||
this.upperLimit = Integer.parseInt(data[1]);
|
||||
if (data.length >= 2) {
|
||||
try {
|
||||
this.lowerLimit = Integer.parseInt(data[0].trim());
|
||||
this.upperLimit = Integer.parseInt(data[1].trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
// malformed legacy data — keep the constructed defaults
|
||||
}
|
||||
}
|
||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-11
@@ -263,22 +263,29 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
||||
} else {
|
||||
String[] data = wiredData.split(":");
|
||||
|
||||
int itemCount = Integer.parseInt(data[0]);
|
||||
if (data.length >= 5) {
|
||||
try {
|
||||
int itemCount = Integer.parseInt(data[0]);
|
||||
|
||||
String[] items = data[1].split(";");
|
||||
String[] items = data[1].split(";");
|
||||
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
String[] stuff = items[i].split("-");
|
||||
for (int i = 0; i < itemCount && i < items.length; i++) {
|
||||
String[] stuff = items[i].split("-");
|
||||
|
||||
if (stuff.length >= 6)
|
||||
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])));
|
||||
else if (stuff.length >= 5)
|
||||
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4])));
|
||||
if (stuff.length >= 6)
|
||||
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])));
|
||||
else if (stuff.length >= 5)
|
||||
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4])));
|
||||
}
|
||||
|
||||
this.state = data[2].equals("1");
|
||||
this.direction = data[3].equals("1");
|
||||
this.position = data[4].equals("1");
|
||||
} 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.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||
this.quantifier = QUANTIFIER_ALL;
|
||||
|
||||
+8
-2
@@ -64,8 +64,14 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
|
||||
this.userSource = data.userSource;
|
||||
} else {
|
||||
String[] data = wiredData.split(":");
|
||||
this.lowerLimit = Integer.parseInt(data[0]);
|
||||
this.upperLimit = Integer.parseInt(data[1]);
|
||||
if (data.length >= 2) {
|
||||
try {
|
||||
this.lowerLimit = Integer.parseInt(data[0].trim());
|
||||
this.upperLimit = Integer.parseInt(data[1].trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
// malformed legacy data — keep the constructed defaults
|
||||
}
|
||||
}
|
||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -20,12 +20,13 @@ import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.incoming.wired.WiredSaveException;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer;
|
||||
import gnu.trove.procedure.TObjectProcedure;
|
||||
import gnu.trove.set.hash.THashSet;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
public static final int LIMIT_ONCE = 0;
|
||||
@@ -37,10 +38,10 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
|
||||
public int limit;
|
||||
public int limitationInterval;
|
||||
public int given;
|
||||
public AtomicInteger given = new AtomicInteger(0);
|
||||
public int rewardTime;
|
||||
public boolean uniqueRewards;
|
||||
public THashSet<WiredGiveRewardItem> rewardItems = new THashSet<>();
|
||||
public List<WiredGiveRewardItem> rewardItems = new CopyOnWriteArrayList<>();
|
||||
public int userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||
|
||||
public WiredEffectGiveReward(ResultSet set, Item baseItem) throws SQLException {
|
||||
@@ -71,9 +72,8 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
|
||||
@Override
|
||||
public String getWiredData() {
|
||||
|
||||
ArrayList<WiredGiveRewardItem> rewards = new ArrayList<>(this.rewardItems);
|
||||
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given, this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
|
||||
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given.get(), this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,7 +84,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||
this.setDelay(data.delay);
|
||||
this.limit = data.limit;
|
||||
this.given = data.given;
|
||||
this.given.set(data.given);
|
||||
this.rewardTime = data.reward_time;
|
||||
this.uniqueRewards = data.unique_rewards;
|
||||
this.limitationInterval = data.limit_interval;
|
||||
@@ -96,7 +96,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
String[] data = wiredData.split(":");
|
||||
if (data.length > 0) {
|
||||
this.limit = Integer.parseInt(data[0]);
|
||||
this.given = Integer.parseInt(data[1]);
|
||||
this.given.set(Integer.parseInt(data[1]));
|
||||
this.rewardTime = Integer.parseInt(data[2]);
|
||||
this.uniqueRewards = data[3].equals("1");
|
||||
this.limitationInterval = Integer.parseInt(data[4]);
|
||||
@@ -127,7 +127,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
public void onPickUp() {
|
||||
this.limit = 0;
|
||||
this.limitationInterval = 0;
|
||||
this.given = 0;
|
||||
this.given.set(0);
|
||||
this.rewardTime = 0;
|
||||
this.uniqueRewards = false;
|
||||
this.rewardItems.clear();
|
||||
@@ -192,7 +192,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
this.limit = settings.getIntParams()[2];
|
||||
this.limitationInterval = settings.getIntParams()[3];
|
||||
this.userSource = settings.getIntParams()[4];
|
||||
this.given = 0;
|
||||
this.given.set(0);
|
||||
|
||||
String data = settings.getStringParam();
|
||||
|
||||
@@ -276,15 +276,15 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
}
|
||||
|
||||
public int getGiven() {
|
||||
return this.given;
|
||||
return this.given.get();
|
||||
}
|
||||
|
||||
public void setGiven(int given) {
|
||||
this.given = given;
|
||||
this.given.set(given);
|
||||
}
|
||||
|
||||
public void incrementGiven() {
|
||||
this.given++;
|
||||
this.given.incrementAndGet();
|
||||
}
|
||||
|
||||
public int getRewardTime() {
|
||||
@@ -303,11 +303,11 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
this.uniqueRewards = uniqueRewards;
|
||||
}
|
||||
|
||||
public THashSet<WiredGiveRewardItem> getRewardItems() {
|
||||
public List<WiredGiveRewardItem> getRewardItems() {
|
||||
return this.rewardItems;
|
||||
}
|
||||
|
||||
public void setRewardItems(THashSet<WiredGiveRewardItem> rewardItems) {
|
||||
public void setRewardItems(List<WiredGiveRewardItem> rewardItems) {
|
||||
this.rewardItems = rewardItems;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -190,10 +190,15 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement
|
||||
}
|
||||
|
||||
for (String s : data[3].split("\r")) {
|
||||
HabboItem item = room.getHabboItem(Integer.parseInt(s));
|
||||
if (s.trim().isEmpty()) continue;
|
||||
try {
|
||||
HabboItem item = room.getHabboItem(Integer.parseInt(s.trim()));
|
||||
|
||||
if (item != null)
|
||||
this.items.add(item);
|
||||
if (item != null)
|
||||
this.items.add(item);
|
||||
} catch (NumberFormatException ignored) {
|
||||
// skip malformed furni id token
|
||||
}
|
||||
}
|
||||
}
|
||||
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect {
|
||||
@Override
|
||||
public boolean execute(InteractionWiredTrigger object) {
|
||||
if (!object.isTriggeredByRoomUnit()) {
|
||||
invalidTriggers.add(object.getId());
|
||||
invalidTriggers.add(object.getBaseItem().getSpriteId());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -252,7 +252,7 @@ public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect {
|
||||
@Override
|
||||
public boolean execute(InteractionWiredTrigger object) {
|
||||
if (!object.isTriggeredByRoomUnit()) {
|
||||
invalidTriggers.add(object.getId());
|
||||
invalidTriggers.add(object.getBaseItem().getSpriteId());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+64
-44
@@ -227,6 +227,18 @@ public final class WiredVariableReferenceSupport {
|
||||
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all cached shared-variable assignments belonging to a room. Both
|
||||
* caches are keyed "roomId:itemId[:userId]", so the trailing colon makes the
|
||||
* prefix match the exact room id. Called on room dispose so the static caches
|
||||
* don't retain entries for the JVM lifetime.
|
||||
*/
|
||||
public static void invalidateRoom(int roomId) {
|
||||
String prefix = roomId + ":";
|
||||
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||
ROOM_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||
}
|
||||
|
||||
public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) {
|
||||
if (reference == null || !reference.isRoomReference()) {
|
||||
return null;
|
||||
@@ -384,61 +396,69 @@ public final class WiredVariableReferenceSupport {
|
||||
}
|
||||
|
||||
private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
|
||||
if (assignment.getValue() == null) {
|
||||
statement.setNull(4, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
statement.setInt(4, assignment.getValue());
|
||||
if (assignment.getValue() == null) {
|
||||
statement.setNull(4, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
statement.setInt(4, assignment.getValue());
|
||||
}
|
||||
|
||||
statement.setInt(5, assignment.getCreatedAt());
|
||||
statement.setInt(6, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
|
||||
statement.setInt(5, assignment.getCreatedAt());
|
||||
statement.setInt(6, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void deleteSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void upsertSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, SharedRoomAssignment assignment) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.setInt(3, assignment.getValue());
|
||||
statement.setInt(4, 0);
|
||||
statement.setInt(5, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.setInt(3, assignment.getValue());
|
||||
statement.setInt(4, 0);
|
||||
statement.setInt(5, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void deleteSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String createDefinitionPrefix(int sourceRoomId, int sourceVariableItemId) {
|
||||
|
||||
+5
-1
@@ -123,7 +123,11 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements
|
||||
@Override
|
||||
public boolean saveData(WiredSettings settings) {
|
||||
if (settings.getIntParams().length < 1) return false;
|
||||
this.repeatTime = settings.getIntParams()[0] * 5000;
|
||||
int interval = settings.getIntParams()[0];
|
||||
if (interval < 1) {
|
||||
interval = 1;
|
||||
}
|
||||
this.repeatTime = interval * 5000;
|
||||
// No accumulated time reset needed - using global tick count
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.eu.habbo.habbohotel.mentions;
|
||||
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HabboMention {
|
||||
|
||||
public static final int TYPE_DIRECT = 0;
|
||||
public static final int TYPE_ROOM = 1;
|
||||
|
||||
private final int id;
|
||||
private final int targetUserId;
|
||||
private final int senderUserId;
|
||||
private final String senderUsername;
|
||||
private final int roomId;
|
||||
private final String roomName;
|
||||
private final String message;
|
||||
private final int mentionType;
|
||||
private final int timestamp;
|
||||
private final boolean read;
|
||||
private final String senderFigure;
|
||||
|
||||
public HabboMention(ResultSet set) throws SQLException {
|
||||
this.id = set.getInt("id");
|
||||
this.targetUserId = set.getInt("target_user_id");
|
||||
this.senderUserId = set.getInt("sender_user_id");
|
||||
this.senderUsername = set.getString("sender_username");
|
||||
this.roomId = set.getInt("room_id");
|
||||
this.roomName = set.getString("room_name");
|
||||
this.message = set.getString("message");
|
||||
this.mentionType = set.getInt("mention_type");
|
||||
this.timestamp = set.getInt("timestamp");
|
||||
this.read = set.getInt("read") == 1;
|
||||
this.senderFigure = hasSenderFigure(set) ? set.getString("sender_figure") : "";
|
||||
}
|
||||
|
||||
private static boolean hasSenderFigure(ResultSet set) {
|
||||
try {
|
||||
set.findColumn("sender_figure");
|
||||
return true;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public HabboMention(int targetUserId, int id, Habbo sender, Room room, String roomName, String message, int mentionType, int timestamp) {
|
||||
this.id = id;
|
||||
this.targetUserId = targetUserId;
|
||||
this.senderUserId = sender.getHabboInfo().getId();
|
||||
this.senderUsername = sender.getHabboInfo().getUsername();
|
||||
this.roomId = room.getId();
|
||||
this.roomName = roomName;
|
||||
this.message = message;
|
||||
this.mentionType = mentionType;
|
||||
this.timestamp = timestamp;
|
||||
this.read = false;
|
||||
this.senderFigure = sender.getHabboInfo().getLook();
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public int getTargetUserId() {
|
||||
return this.targetUserId;
|
||||
}
|
||||
|
||||
public int getSenderUserId() {
|
||||
return this.senderUserId;
|
||||
}
|
||||
|
||||
public String getSenderUsername() {
|
||||
return this.senderUsername;
|
||||
}
|
||||
|
||||
public int getRoomId() {
|
||||
return this.roomId;
|
||||
}
|
||||
|
||||
public String getRoomName() {
|
||||
return this.roomName;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public int getMentionType() {
|
||||
return this.mentionType;
|
||||
}
|
||||
|
||||
public int getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public boolean isRead() {
|
||||
return this.read;
|
||||
}
|
||||
|
||||
public String getSenderFigure() {
|
||||
return this.senderFigure == null ? "" : this.senderFigure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
package com.eu.habbo.habbohotel.mentions;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.messenger.MessengerBuddy;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatType;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class MentionManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class);
|
||||
|
||||
private static final int ROOM_NAME_MAX_LENGTH = 64;
|
||||
private static final int MESSAGE_MAX_LENGTH = 255;
|
||||
private final ConcurrentHashMap<Integer, Long> cooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> roomBroadcastCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> requestListCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> markReadCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> markAllCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> deleteCooldowns = new ConcurrentHashMap<>();
|
||||
|
||||
private volatile long lastPrune = System.currentTimeMillis();
|
||||
private static final long PRUNE_INTERVAL_MS = 5 * 60_000L;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
|
||||
}
|
||||
|
||||
public enum BroadcastScope {
|
||||
NONE,
|
||||
ROOM,
|
||||
FRIENDS,
|
||||
EVERYONE
|
||||
}
|
||||
|
||||
public static final String PERMISSION_EVERYONE = "acc_mention_everyone";
|
||||
public static final String PERMISSION_FRIENDS = "acc_mention_friends";
|
||||
|
||||
private Set<String> parseAliases(String configKey, String defaultValue) {
|
||||
Set<String> aliases = new HashSet<>();
|
||||
String raw = Emulator.getConfig().getValue(configKey, defaultValue);
|
||||
for (String alias : raw.split(",")) {
|
||||
String trimmed = alias.trim().toLowerCase();
|
||||
if (!trimmed.isEmpty()) {
|
||||
aliases.add(trimmed);
|
||||
}
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private Set<String> roomAliases() {
|
||||
return parseAliases("mentions.room.aliases", "room,stanza");
|
||||
}
|
||||
|
||||
private Set<String> friendsAliases() {
|
||||
return parseAliases("mentions.friends.aliases", "friends,amici");
|
||||
}
|
||||
|
||||
private Set<String> everyoneAliases() {
|
||||
return parseAliases("mentions.everyone.aliases", "all,everyone,tutti");
|
||||
}
|
||||
|
||||
private BroadcastScope classifyAlias(String alias,
|
||||
Set<String> everyone,
|
||||
Set<String> friends,
|
||||
Set<String> room) {
|
||||
if (alias.isEmpty()) return BroadcastScope.NONE;
|
||||
if (everyone.contains(alias)) return BroadcastScope.EVERYONE;
|
||||
if (friends.contains(alias)) return BroadcastScope.FRIENDS;
|
||||
if (room.contains(alias)) return BroadcastScope.ROOM;
|
||||
return BroadcastScope.NONE;
|
||||
}
|
||||
|
||||
public void process(Habbo sender, Room room, String message, RoomChatType type) {
|
||||
try {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender == null || room == null || message == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.isEmpty() || message.indexOf('@') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int senderId = sender.getHabboInfo().getId();
|
||||
long now = System.currentTimeMillis();
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
|
||||
Long last = this.cooldowns.get(senderId);
|
||||
if (last != null && (now - last) < cooldownMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> roomAliases = this.roomAliases();
|
||||
Set<String> friendsAliases = this.friendsAliases();
|
||||
Set<String> everyoneAliases = this.everyoneAliases();
|
||||
|
||||
BroadcastScope broadcastScope = BroadcastScope.NONE;
|
||||
LinkedHashSet<String> directTokens = new LinkedHashSet<>();
|
||||
|
||||
for (String token : message.split("\\s+")) {
|
||||
if (token.length() < 2 || token.charAt(0) != '@') {
|
||||
continue;
|
||||
}
|
||||
|
||||
String raw = token.substring(1);
|
||||
String aliasCandidate = trimTrailingPunctuation(raw).toLowerCase();
|
||||
|
||||
BroadcastScope scope = this.classifyAlias(aliasCandidate, everyoneAliases, friendsAliases, roomAliases);
|
||||
|
||||
if (scope != BroadcastScope.NONE) {
|
||||
if (scope.ordinal() > broadcastScope.ordinal()) {
|
||||
broadcastScope = scope;
|
||||
}
|
||||
} else if (!raw.isEmpty()) {
|
||||
directTokens.add(raw);
|
||||
}
|
||||
}
|
||||
|
||||
if (broadcastScope == BroadcastScope.EVERYONE && !sender.hasPermission(PERMISSION_EVERYONE)) {
|
||||
broadcastScope = BroadcastScope.NONE;
|
||||
} else if (broadcastScope == BroadcastScope.FRIENDS && !sender.hasPermission(PERMISSION_FRIENDS)) {
|
||||
broadcastScope = BroadcastScope.NONE;
|
||||
}
|
||||
|
||||
if (broadcastScope == BroadcastScope.NONE && directTokens.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (broadcastScope != BroadcastScope.NONE) {
|
||||
long roomCooldownMs = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
|
||||
Long lastRoom = this.roomBroadcastCooldowns.get(senderId);
|
||||
if (lastRoom != null && (now - lastRoom) < roomCooldownMs) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50);
|
||||
if (maxTargets <= 0) maxTargets = 1;
|
||||
int maxDirectTokens = Math.min(directTokens.size(), maxTargets);
|
||||
|
||||
List<Habbo> targets = new ArrayList<>();
|
||||
Set<Integer> seen = new HashSet<>();
|
||||
|
||||
switch (broadcastScope) {
|
||||
case EVERYONE:
|
||||
this.collectEveryoneTargets(senderId, targets, seen, maxTargets);
|
||||
break;
|
||||
case FRIENDS:
|
||||
this.collectFriendsTargets(sender, senderId, targets, seen, maxTargets);
|
||||
break;
|
||||
case ROOM:
|
||||
this.collectRoomTargets(room, senderId, targets, seen, maxTargets, true);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
int processed = 0;
|
||||
for (String token : directTokens) {
|
||||
if (processed++ >= maxDirectTokens) break;
|
||||
Habbo habbo = this.resolveHabbo(room, token);
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
|
||||
continue;
|
||||
}
|
||||
if (!acceptsMention(habbo, false)) {
|
||||
continue;
|
||||
}
|
||||
if (seen.add(habbo.getHabboInfo().getId())) {
|
||||
targets.add(habbo);
|
||||
}
|
||||
if (targets.size() >= maxTargets) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cooldowns.put(senderId, now);
|
||||
if (broadcastScope != BroadcastScope.NONE) this.roomBroadcastCooldowns.put(senderId, now);
|
||||
this.pruneCooldownsIfDue(now);
|
||||
|
||||
int mentionType = (broadcastScope != BroadcastScope.NONE) ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT;
|
||||
int timestamp = Emulator.getIntUnixTimestamp();
|
||||
String roomName = truncate(room.getName(), ROOM_NAME_MAX_LENGTH);
|
||||
String storedMessage = truncate(message, MESSAGE_MAX_LENGTH);
|
||||
|
||||
for (Habbo target : targets) {
|
||||
this.store(target, sender, room, storedMessage, mentionType, timestamp, roomName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to process mentions.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectRoomTargets(Room room, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets, boolean isBroadcast) {
|
||||
for (Habbo habbo : room.getHabbos()) {
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
|
||||
if (!acceptsMention(habbo, isBroadcast)) continue;
|
||||
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
|
||||
if (targets.size() >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
|
||||
private void collectFriendsTargets(Habbo sender, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
|
||||
if (sender.getMessenger() == null) return;
|
||||
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
|
||||
for (MessengerBuddy buddy : sender.getMessenger().getFriends().values()) {
|
||||
if (buddy == null) continue;
|
||||
int buddyId = buddy.getId();
|
||||
if (buddyId == senderId) continue;
|
||||
Habbo online = habboManager.getHabbo(buddyId);
|
||||
if (online == null) continue;
|
||||
if (!acceptsMention(online, true)) continue;
|
||||
if (seen.add(buddyId)) targets.add(online);
|
||||
if (targets.size() >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
|
||||
private void collectEveryoneTargets(int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
|
||||
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
|
||||
if (!acceptsMention(habbo, true)) continue;
|
||||
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
|
||||
if (targets.size() >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean acceptsMention(Habbo recipient, boolean isBroadcast) {
|
||||
if (recipient == null) return false;
|
||||
if (recipient.getClient() == null) return false;
|
||||
if (recipient.getHabboStats() == null) return false;
|
||||
if (!recipient.getHabboStats().mentionsEnabled()) return false;
|
||||
if (isBroadcast && !recipient.getHabboStats().massMentionsEnabled()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void store(Habbo target, Habbo sender, Room room, String message, int mentionType, int timestamp, String roomName) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO habbo_mentions (target_user_id, sender_user_id, sender_username, room_id, room_name, message, mention_type, timestamp, `read`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
statement.setInt(1, target.getHabboInfo().getId());
|
||||
statement.setInt(2, sender.getHabboInfo().getId());
|
||||
statement.setString(3, sender.getHabboInfo().getUsername());
|
||||
statement.setInt(4, room.getId());
|
||||
statement.setString(5, roomName);
|
||||
statement.setString(6, message);
|
||||
statement.setInt(7, mentionType);
|
||||
statement.setInt(8, timestamp);
|
||||
statement.executeUpdate();
|
||||
|
||||
int generatedId = 0;
|
||||
try (ResultSet keys = statement.getGeneratedKeys()) {
|
||||
if (keys.next()) {
|
||||
generatedId = keys.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (generatedId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
HabboMention mention = new HabboMention(target.getHabboInfo().getId(), generatedId, sender, room, roomName, message, mentionType, timestamp);
|
||||
|
||||
if (target.getClient() != null) {
|
||||
target.getClient().sendResponse(new com.eu.habbo.messages.outgoing.mentions.MentionReceivedComposer(mention));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store mention.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<HabboMention> getMentions(int userId, int limit) {
|
||||
List<HabboMention> mentions = new ArrayList<>();
|
||||
if (limit <= 0) limit = 50;
|
||||
if (limit > 200) limit = 200;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT habbo_mentions.*, users.look AS sender_figure FROM habbo_mentions LEFT JOIN users ON users.id = habbo_mentions.sender_user_id WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, limit);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
mentions.add(new HabboMention(set));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to load mentions.", e);
|
||||
}
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public void markRead(int userId, int mode, int mentionId) {
|
||||
if (mode != 0 && mode != 1) return;
|
||||
if (mode == 1 && mentionId <= 0) return;
|
||||
|
||||
String query = mode == 1
|
||||
? "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND id = ? AND `read` = 0"
|
||||
: "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND `read` = 0";
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(query)) {
|
||||
statement.setInt(1, userId);
|
||||
if (mode == 1) {
|
||||
statement.setInt(2, mentionId);
|
||||
}
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to mark mentions as read.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(int userId, int mentionId) {
|
||||
if (mentionId <= 0) return;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"DELETE FROM habbo_mentions WHERE target_user_id = ? AND id = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, mentionId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete mention.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean tryAcquireRequestList(int userId) {
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
|
||||
return tryAcquire(this.requestListCooldowns, userId, cooldownMs);
|
||||
}
|
||||
|
||||
public boolean tryAcquireMarkRead(int userId, int mode) {
|
||||
long cooldownMs;
|
||||
ConcurrentHashMap<Integer, Long> bucket;
|
||||
if (mode == 1) {
|
||||
cooldownMs = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
|
||||
bucket = this.markReadCooldowns;
|
||||
} else {
|
||||
cooldownMs = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
|
||||
bucket = this.markAllCooldowns;
|
||||
}
|
||||
return tryAcquire(bucket, userId, cooldownMs);
|
||||
}
|
||||
|
||||
public boolean tryAcquireDelete(int userId) {
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
|
||||
return tryAcquire(this.deleteCooldowns, userId, cooldownMs);
|
||||
}
|
||||
|
||||
private boolean tryAcquire(ConcurrentHashMap<Integer, Long> bucket, int userId, long cooldownMs) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = bucket.get(userId);
|
||||
if (last != null && (now - last) < cooldownMs) {
|
||||
return false;
|
||||
}
|
||||
bucket.put(userId, now);
|
||||
this.pruneCooldownsIfDue(now);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void pruneCooldownsIfDue(long now) {
|
||||
if (now - this.lastPrune < PRUNE_INTERVAL_MS) return;
|
||||
this.lastPrune = now;
|
||||
|
||||
long mentionWindow = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
|
||||
long roomWindow = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
|
||||
long requestWindow = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
|
||||
long markReadWindow = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
|
||||
long markAllWindow = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
|
||||
long deleteWindow = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
|
||||
|
||||
prune(this.cooldowns, now, mentionWindow);
|
||||
prune(this.roomBroadcastCooldowns, now, roomWindow);
|
||||
prune(this.requestListCooldowns, now, requestWindow);
|
||||
prune(this.markReadCooldowns, now, markReadWindow);
|
||||
prune(this.markAllCooldowns, now, markAllWindow);
|
||||
prune(this.deleteCooldowns, now, deleteWindow);
|
||||
}
|
||||
|
||||
private static void prune(ConcurrentHashMap<Integer, Long> bucket, long now, long windowMs) {
|
||||
Iterator<Map.Entry<Integer, Long>> it = bucket.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<Integer, Long> entry = it.next();
|
||||
Long value = entry.getValue();
|
||||
if (value == null || (now - value) >= windowMs) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TRAILING_PUNCTUATION = ".,!?;:)]}\"'";
|
||||
|
||||
private static String trimTrailingPunctuation(String value) {
|
||||
int end = value.length();
|
||||
while (end > 0 && TRAILING_PUNCTUATION.indexOf(value.charAt(end - 1)) >= 0) {
|
||||
end--;
|
||||
}
|
||||
return value.substring(0, end);
|
||||
}
|
||||
|
||||
private static String truncate(String value, int max) {
|
||||
if (value == null) return "";
|
||||
if (value.length() <= max) return value;
|
||||
return value.substring(0, max);
|
||||
}
|
||||
|
||||
private boolean isBotOrPetName(Room room, String token) {
|
||||
if (room == null || token == null || token.isEmpty()) return false;
|
||||
|
||||
List<com.eu.habbo.habbohotel.bots.Bot> bots = room.getBots(token);
|
||||
if (bots != null && !bots.isEmpty()) return true;
|
||||
|
||||
if (room.getUnitManager() != null && room.getUnitManager().getPets() != null) {
|
||||
for (com.eu.habbo.habbohotel.pets.Pet pet : room.getUnitManager().getPets()) {
|
||||
if (pet != null && pet.getName() != null && pet.getName().equalsIgnoreCase(token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Habbo resolveHabbo(Room room, String rawToken) {
|
||||
if (isBotOrPetName(room, rawToken)) {
|
||||
return null;
|
||||
}
|
||||
String trimmedForBotCheck = trimTrailingPunctuation(rawToken);
|
||||
if (!trimmedForBotCheck.equals(rawToken) && isBotOrPetName(room, trimmedForBotCheck)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Habbo habbo = room.getHabbo(rawToken);
|
||||
if (habbo != null) {
|
||||
return habbo;
|
||||
}
|
||||
|
||||
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
|
||||
habbo = habboManager.getHabbo(rawToken);
|
||||
if (habbo != null) {
|
||||
return habbo;
|
||||
}
|
||||
String trimmed = trimTrailingPunctuation(rawToken);
|
||||
if (!trimmed.isEmpty() && !trimmed.equals(rawToken)) {
|
||||
habbo = room.getHabbo(trimmed);
|
||||
if (habbo != null) {
|
||||
return habbo;
|
||||
}
|
||||
return habboManager.getHabbo(trimmed);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public class Messenger {
|
||||
public static THashSet<MessengerBuddy> searchUsers(String username) {
|
||||
THashSet<MessengerBuddy> users = new THashSet<>();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE username LIKE ? ORDER BY username ASC LIMIT " + Emulator.getConfig().getInt("hotel.messenger.search.maxresults"))) {
|
||||
statement.setString(1, username + "%");
|
||||
statement.setString(1, com.eu.habbo.util.SqlLikeEscaper.escape(username) + "%");
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
users.add(new MessengerBuddy(set, false));
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.eu.habbo.habbohotel.modtool;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* Append-only audit trail for privileged housekeeping/admin actions (rank grants,
|
||||
* currency grants, etc.). There was previously no record of which operator did
|
||||
* what to whom. Writes are dispatched off the calling thread; the backing table
|
||||
* is created on first use so no manual migration is required.
|
||||
*/
|
||||
public final class HousekeepingAuditLog {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(HousekeepingAuditLog.class);
|
||||
|
||||
private static volatile boolean tableReady = false;
|
||||
|
||||
private HousekeepingAuditLog() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a privileged action asynchronously.
|
||||
*
|
||||
* @param operatorId the acting staff member's user id
|
||||
* @param operatorName the acting staff member's username
|
||||
* @param action a short action key, e.g. {@code "user.set_rank"}
|
||||
* @param targetUserId the affected user's id (0 if not applicable)
|
||||
* @param detail free-form detail, e.g. {@code "rankId=6"} (capped to 512 chars)
|
||||
* @param ip the operator's IP, for correlation
|
||||
*/
|
||||
public static void log(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
|
||||
Emulator.getThreading().run(() -> writeEntry(operatorId, operatorName, action, targetUserId, detail, ip));
|
||||
}
|
||||
|
||||
private static void writeEntry(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
|
||||
ensureTable();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO housekeeping_log (operator_id, operator_name, action, target_user_id, detail, ip, timestamp) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)")) {
|
||||
statement.setInt(1, operatorId);
|
||||
statement.setString(2, operatorName != null ? operatorName : "");
|
||||
statement.setString(3, action != null ? action : "");
|
||||
statement.setInt(4, targetUserId);
|
||||
statement.setString(5, truncate(detail));
|
||||
statement.setString(6, ip != null ? ip : "");
|
||||
statement.setInt(7, Emulator.getIntUnixTimestamp());
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to write housekeeping audit log entry", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncate(String detail) {
|
||||
if (detail == null) return "";
|
||||
return detail.length() > 512 ? detail.substring(0, 512) : detail;
|
||||
}
|
||||
|
||||
private static void ensureTable() {
|
||||
if (tableReady) {
|
||||
return;
|
||||
}
|
||||
synchronized (HousekeepingAuditLog.class) {
|
||||
if (tableReady) {
|
||||
return;
|
||||
}
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute(
|
||||
"CREATE TABLE IF NOT EXISTS housekeeping_log (" +
|
||||
"id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
|
||||
"operator_id INT NOT NULL, " +
|
||||
"operator_name VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"action VARCHAR(64) NOT NULL, " +
|
||||
"target_user_id INT NOT NULL DEFAULT 0, " +
|
||||
"detail VARCHAR(512) NOT NULL DEFAULT '', " +
|
||||
"ip VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"timestamp INT NOT NULL, " +
|
||||
"PRIMARY KEY (id), " +
|
||||
"KEY idx_operator (operator_id), " +
|
||||
"KEY idx_target (target_user_id), " +
|
||||
"KEY idx_timestamp (timestamp)" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
tableReady = true;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to create housekeeping_log table", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,7 +378,9 @@ public class ModToolManager {
|
||||
statement.setString(6, reason);
|
||||
statement.setString(7, type.getType());
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
statement.executeUpdate();
|
||||
|
||||
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||
if (set.next()) {
|
||||
try (PreparedStatement selectBanStatement = connection.prepareStatement("SELECT * FROM bans WHERE id = ? LIMIT 1")) {
|
||||
selectBanStatement.setInt(1, set.getInt(1));
|
||||
@@ -434,6 +436,10 @@ public class ModToolManager {
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUserId);
|
||||
HabboInfo offlineInfo = target != null ? target.getHabboInfo() : HabboManager.getOfflineHabboInfo(targetUserId);
|
||||
|
||||
if (offlineInfo == null) {
|
||||
return bans;
|
||||
}
|
||||
|
||||
if (moderator.getHabboInfo().getRank().getId() < offlineInfo.getRank().getId()) {
|
||||
return bans;
|
||||
}
|
||||
@@ -454,7 +460,7 @@ public class ModToolManager {
|
||||
bans.add(ban);
|
||||
|
||||
if (target != null) {
|
||||
Emulator.getGameServer().getGameClientManager().disposeClient(target.getClient());
|
||||
Emulator.getGameServer().getGameClientManager().forceDisposeClient(target.getClient());
|
||||
}
|
||||
|
||||
if ((type == ModToolBanType.IP || type == ModToolBanType.SUPER) && target != null && !ban.ip.equals("offline")) {
|
||||
@@ -465,7 +471,7 @@ public class ModToolManager {
|
||||
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
||||
Emulator.getThreading().run(ban);
|
||||
bans.add(ban);
|
||||
Emulator.getGameServer().getGameClientManager().disposeClient(h.getClient());
|
||||
Emulator.getGameServer().getGameClientManager().forceDisposeClient(h.getClient());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +483,7 @@ public class ModToolManager {
|
||||
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
|
||||
Emulator.getThreading().run(ban);
|
||||
bans.add(ban);
|
||||
Emulator.getGameServer().getGameClientManager().disposeClient(h.getClient());
|
||||
Emulator.getGameServer().getGameClientManager().forceDisposeClient(h.getClient());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,6 +657,10 @@ public class ModToolManager {
|
||||
sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE));
|
||||
}
|
||||
|
||||
// Reporter (the user who opened the CFH) gets their abusive
|
||||
// counter bumped — the legacy stat shown in the User Info table.
|
||||
bumpUserSettingCounter(issue.senderId, "cfh_abusive");
|
||||
|
||||
this.updateTicketToMods(issue);
|
||||
|
||||
this.removeTicket(issue);
|
||||
@@ -737,4 +747,38 @@ public class ModToolManager {
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments a single integer counter on `users_settings` for the
|
||||
* given user. Used by the moderation sanction handlers to bump the
|
||||
* legacy counters that `ModToolUserInfoComposer` surfaces (cfh_warnings,
|
||||
* cfh_bans, cfh_abusive, tradelock_amount) — historically these were
|
||||
* only ever incremented by the CFH submission path, so a user could
|
||||
* accumulate any number of bans/mutes without the User Info table
|
||||
* reflecting it.
|
||||
*
|
||||
* Restricted to a whitelisted column name to keep the dynamic SQL
|
||||
* safe; the caller passes a Permission-style constant.
|
||||
*/
|
||||
public static void bumpUserSettingCounter(int userId, String column) {
|
||||
switch (column) {
|
||||
case "cfh_warnings":
|
||||
case "cfh_bans":
|
||||
case "cfh_abusive":
|
||||
case "tradelock_amount":
|
||||
break;
|
||||
default:
|
||||
LOGGER.warn("Refusing to bump unrecognized user_settings column: {}", column);
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE users_settings SET " + column + " = " + column + " + 1 WHERE user_id = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception bumping {} for user {}", column, userId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ public class WordFilter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WordFilter.class);
|
||||
|
||||
private static final Pattern DIACRITICS_AND_FRIENDS = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
|
||||
//Configuration. Loaded from database & updated accordingly.
|
||||
public static boolean ENABLED_FRIENDCHAT = true;
|
||||
public static String DEFAULT_REPLACEMENT = "bobba";
|
||||
protected THashSet<WordFilterWord> autoReportWords = new THashSet<>();
|
||||
@@ -63,10 +62,12 @@ public class WordFilter {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (word.autoReport)
|
||||
this.autoReportWords.add(word);
|
||||
else if (word.hideMessage)
|
||||
this.hideMessageWords.add(word);
|
||||
if (!word.prefixOnly) {
|
||||
if (word.autoReport)
|
||||
this.autoReportWords.add(word);
|
||||
else if (word.hideMessage)
|
||||
this.hideMessageWords.add(word);
|
||||
}
|
||||
|
||||
this.words.add(word);
|
||||
}
|
||||
@@ -146,6 +147,8 @@ public class WordFilter {
|
||||
while (iterator.hasNext()) {
|
||||
WordFilterWord word = (WordFilterWord) iterator.next();
|
||||
|
||||
if (word.prefixOnly) continue;
|
||||
|
||||
if (StringUtils.containsIgnoreCase(filteredMessage, word.key)) {
|
||||
if (habbo != null) {
|
||||
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
|
||||
@@ -179,6 +182,8 @@ public class WordFilter {
|
||||
while (iterator.hasNext()) {
|
||||
WordFilterWord word = (WordFilterWord) iterator.next();
|
||||
|
||||
if (word.prefixOnly) continue;
|
||||
|
||||
if (StringUtils.containsIgnoreCase(message, word.key)) {
|
||||
if (habbo != null) {
|
||||
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
|
||||
|
||||
@@ -9,6 +9,7 @@ public class WordFilterWord {
|
||||
public final boolean hideMessage;
|
||||
public final boolean autoReport;
|
||||
public final int muteTime;
|
||||
public final boolean prefixOnly;
|
||||
|
||||
public WordFilterWord(ResultSet set) throws SQLException {
|
||||
this.key = set.getString("key");
|
||||
@@ -16,13 +17,27 @@ public class WordFilterWord {
|
||||
this.hideMessage = set.getInt("hide") == 1;
|
||||
this.autoReport = set.getInt("report") == 1;
|
||||
this.muteTime = set.getInt("mute");
|
||||
this.prefixOnly = readBooleanColumn(set, "prefix_only");
|
||||
}
|
||||
|
||||
public WordFilterWord(String key, String replacement) {
|
||||
this(key, replacement, false);
|
||||
}
|
||||
|
||||
public WordFilterWord(String key, String replacement, boolean prefixOnly) {
|
||||
this.key = key;
|
||||
this.replacement = replacement;
|
||||
this.hideMessage = false;
|
||||
this.autoReport = false;
|
||||
this.muteTime = 0;
|
||||
this.prefixOnly = prefixOnly;
|
||||
}
|
||||
|
||||
private static boolean readBooleanColumn(ResultSet set, String column) {
|
||||
try {
|
||||
return set.getInt(column) == 1;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public class Permission {
|
||||
public static String ACC_SEE_WHISPERS = "acc_see_whispers";
|
||||
public static String ACC_SEE_TENTCHAT = "acc_see_tentchat";
|
||||
public static String ACC_SUPERWIRED = "acc_superwired";
|
||||
public static String ACC_HOUSEKEEPING = "acc_housekeeping";
|
||||
public static String ACC_SUPPORTTOOL = "acc_supporttool";
|
||||
public static String ACC_UNKICKABLE = "acc_unkickable";
|
||||
public static String ACC_GUILDGATE = "acc_guildgate";
|
||||
|
||||
@@ -115,19 +115,21 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
public final Object roomUnitLock = new Object();
|
||||
public final ConcurrentHashMap<RoomTile, THashSet<HabboItem>> tileCache = new ConcurrentHashMap<>();
|
||||
public final List<Integer> userVotes;
|
||||
private final TIntArrayList rights;
|
||||
private final TIntIntHashMap mutedHabbos;
|
||||
private final TIntObjectHashMap<RoomBan> bannedHabbos;
|
||||
private final Set<Game> games;
|
||||
private final TIntObjectMap<RoomMoodlightData> moodlightData;
|
||||
public volatile double lastCycleCpuMs = 0.0;
|
||||
public volatile String lastCycleThread = "N/A";
|
||||
|
||||
private final Object loadLock = new Object();
|
||||
//Use appropriately. Could potentially cause memory leaks when used incorrectly.
|
||||
public volatile boolean preventUnloading = false;
|
||||
public volatile boolean preventUncaching = false;
|
||||
public Set<ServerMessage> scheduledComposers = ConcurrentHashMap.newKeySet();
|
||||
public Set<Runnable> scheduledTasks = ConcurrentHashMap.newKeySet();
|
||||
public final java.util.concurrent.ConcurrentLinkedQueue<Runnable> scheduledTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
|
||||
public String wordQuiz = "";
|
||||
public int noVotes = 0;
|
||||
public int yesVotes = 0;
|
||||
@@ -195,6 +197,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK;
|
||||
private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK;
|
||||
private boolean youtubeEnabled = false;
|
||||
private boolean soundboardEnabled = false;
|
||||
private String youtubeCurrentVideo = "";
|
||||
private String youtubeSenderName = "";
|
||||
private final java.util.List<String> youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
@@ -202,22 +205,24 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
|
||||
public boolean isYoutubeEnabled() { return this.youtubeEnabled; }
|
||||
public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; }
|
||||
public boolean isSoundboardEnabled() { return this.soundboardEnabled; }
|
||||
public void setSoundboardEnabled(boolean enabled) { this.soundboardEnabled = enabled; }
|
||||
public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; }
|
||||
public String getYoutubeSenderName() { return this.youtubeSenderName; }
|
||||
public java.util.List<String> getYoutubePlaylist() { return this.youtubePlaylist; }
|
||||
public java.util.Set<Integer> getYoutubeWatchers() { return this.youtubeWatchers; }
|
||||
|
||||
public void setYoutubeVideo(String videoId, String senderName, java.util.List<String> playlist) {
|
||||
this.youtubeCurrentVideo = videoId;
|
||||
this.youtubeSenderName = senderName;
|
||||
this.youtubePlaylist.clear();
|
||||
if (playlist != null) this.youtubePlaylist.addAll(playlist);
|
||||
this.youtubeCurrentVideo = videoId;
|
||||
this.youtubeSenderName = senderName;
|
||||
this.youtubePlaylist.clear();
|
||||
if (playlist != null) this.youtubePlaylist.addAll(playlist);
|
||||
}
|
||||
|
||||
public void clearYoutubeVideo() {
|
||||
this.youtubeCurrentVideo = "";
|
||||
this.youtubeSenderName = "";
|
||||
this.youtubePlaylist.clear();
|
||||
this.youtubeCurrentVideo = "";
|
||||
this.youtubeSenderName = "";
|
||||
this.youtubePlaylist.clear();
|
||||
}
|
||||
|
||||
public final THashMap<String, Object> cache;
|
||||
@@ -234,9 +239,9 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.usersMax = set.getInt("users_max");
|
||||
this.score = set.getInt("score");
|
||||
this.category = set.getInt("category");
|
||||
this.floorPaint = set.getString("paper_floor");
|
||||
this.wallPaint = set.getString("paper_wall");
|
||||
this.backgroundPaint = set.getString("paper_landscape");
|
||||
this.floorPaint = set.getString("paper_floor") == null ? "0.0" : set.getString("paper_floor");
|
||||
this.wallPaint = set.getString("paper_wall") == null ? "0.0" : set.getString("paper_wall");
|
||||
this.backgroundPaint = set.getString("paper_landscape") == null ? "0.0" : set.getString("paper_landscape");
|
||||
this.wallSize = set.getInt("thickness_wall");
|
||||
this.wallHeight = set.getInt("wall_height");
|
||||
this.floorSize = set.getInt("thickness_floor");
|
||||
@@ -248,6 +253,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.allowWalkthrough = set.getBoolean("allow_walkthrough");
|
||||
this.hideWall = set.getBoolean("allow_hidewall");
|
||||
try { this.youtubeEnabled = set.getBoolean("youtube_enabled"); } catch (Exception e) { this.youtubeEnabled = false; }
|
||||
try { this.soundboardEnabled = set.getBoolean("soundboard_enabled"); } catch (Exception e) { this.soundboardEnabled = false; }
|
||||
this.chatMode = set.getInt("chat_mode");
|
||||
this.chatWeight = set.getInt("chat_weight");
|
||||
this.chatSpeed = set.getInt("chat_speed");
|
||||
@@ -458,7 +464,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
if (this.loaded || this.loadingInProgress || !this.preLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.loadingInProgress = true;
|
||||
this.loadingFuture = CompletableFuture.runAsync(() -> {
|
||||
this.loadDataInternal();
|
||||
@@ -478,7 +484,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
future = this.loadingFuture;
|
||||
}
|
||||
|
||||
|
||||
if (future != null) {
|
||||
try {
|
||||
future.join();
|
||||
@@ -493,7 +499,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
public void loadData() {
|
||||
CompletableFuture<Void> futureToWait = null;
|
||||
boolean shouldLoad = false;
|
||||
|
||||
|
||||
synchronized (this.loadLock) {
|
||||
if (this.loadingInProgress) {
|
||||
// Get the future to wait on outside the lock
|
||||
@@ -503,7 +509,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
shouldLoad = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait for existing load outside the lock
|
||||
if (futureToWait != null) {
|
||||
try {
|
||||
@@ -513,7 +519,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Load if needed
|
||||
if (shouldLoad) {
|
||||
this.loadDataInternal();
|
||||
@@ -553,7 +559,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try (Connection promoConnection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = promoConnection.prepareStatement(
|
||||
"SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) {
|
||||
"SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) {
|
||||
stmt.setInt(1, this.id);
|
||||
stmt.setInt(2, Emulator.getIntUnixTimestamp());
|
||||
try (ResultSet promoSet = stmt.executeQuery()) {
|
||||
@@ -648,7 +654,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
this.roomCycleTask = Emulator.getThreading().getService()
|
||||
.scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS);
|
||||
.scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception during room load", e);
|
||||
}
|
||||
@@ -667,7 +673,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
item.setExtradata("1");
|
||||
this.updateItem(item);
|
||||
}
|
||||
|
||||
|
||||
// Set loaded flag with lock
|
||||
synchronized (this.loadLock) {
|
||||
this.loaded = true;
|
||||
@@ -684,7 +690,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.layout = Emulator.getGameEnvironment().getRoomManager().loadCustomLayout(this);
|
||||
} else {
|
||||
this.layout = Emulator.getGameEnvironment().getRoomManager()
|
||||
.loadLayout(this.layoutName, this);
|
||||
.loadLayout(this.layoutName, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,7 +722,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.unitManager.clearBots();
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT users.username AS owner_name, bots.* FROM bots INNER JOIN users ON bots.user_id = users.id WHERE room_id = ?")) {
|
||||
"SELECT users.username AS owner_name, bots.* FROM bots INNER JOIN users ON bots.user_id = users.id WHERE room_id = ?")) {
|
||||
statement.setInt(1, this.id);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
@@ -727,11 +733,11 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
b.setRoomUnit(new RoomUnit());
|
||||
b.getRoomUnit().setPathFinderRoom(this);
|
||||
b.getRoomUnit()
|
||||
.setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
|
||||
.setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
|
||||
if (b.getRoomUnit().getCurrentLocation() == null) {
|
||||
b.getRoomUnit().setLocation(this.getLayout().getDoorTile());
|
||||
b.getRoomUnit()
|
||||
.setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
|
||||
.setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
|
||||
} else {
|
||||
b.getRoomUnit().setZ(set.getDouble("z"));
|
||||
b.getRoomUnit().setPreviousLocationZ(set.getDouble("z"));
|
||||
@@ -755,7 +761,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.unitManager.clearPets();
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT users.username as pet_owner_name, users_pets.* FROM users_pets INNER JOIN users ON users_pets.user_id = users.id WHERE room_id = ?")) {
|
||||
"SELECT users.username as pet_owner_name, users_pets.* FROM users_pets INNER JOIN users ON users_pets.user_id = users.id WHERE room_id = ?")) {
|
||||
statement.setInt(1, this.id);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
@@ -765,11 +771,11 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
pet.setRoomUnit(new RoomUnit());
|
||||
pet.getRoomUnit().setPathFinderRoom(this);
|
||||
pet.getRoomUnit()
|
||||
.setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
|
||||
.setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
|
||||
if (pet.getRoomUnit().getCurrentLocation() == null) {
|
||||
pet.getRoomUnit().setLocation(this.getLayout().getDoorTile());
|
||||
pet.getRoomUnit()
|
||||
.setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
|
||||
.setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
|
||||
} else {
|
||||
pet.getRoomUnit().setZ(set.getDouble("z"));
|
||||
pet.getRoomUnit().setRotation(RoomUserRotation.values()[set.getInt("rot")]);
|
||||
@@ -843,7 +849,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
|
||||
THashSet<RoomTile> updatedTiles = new THashSet<>();
|
||||
Rectangle rectangle = RoomLayout.getRectangle(item.getX(), item.getY(),
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation());
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation());
|
||||
|
||||
for (short x = (short) rectangle.x; x < rectangle.x + rectangle.getWidth(); x++) {
|
||||
for (short y = (short) rectangle.y; y < rectangle.y + rectangle.getHeight(); y++) {
|
||||
@@ -872,7 +878,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
Habbo habbo = (picker != null && picker.getHabboInfo().getId() == item.getId() ? picker
|
||||
: Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId()));
|
||||
: Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId()));
|
||||
if (!trackedBuildersClubItem && habbo != null) {
|
||||
habbo.getInventory().getItemsComponent().addItem(item);
|
||||
habbo.getClient().sendResponse(new AddHabboItemComposer(item));
|
||||
@@ -981,8 +987,6 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.scheduledTasks.clear();
|
||||
this.scheduledComposers.clear();
|
||||
|
||||
this.tileCache.clear();
|
||||
|
||||
synchronized (this.mutedHabbos) {
|
||||
this.mutedHabbos.clear();
|
||||
}
|
||||
@@ -1112,7 +1116,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
message.appendInt(this.category);
|
||||
|
||||
String[] tags = Arrays.stream(this.tags.split(";")).filter(t -> !t.isEmpty())
|
||||
.toArray(String[]::new);
|
||||
.toArray(String[]::new);
|
||||
message.appendInt(tags.length);
|
||||
for (String s : tags) {
|
||||
message.appendString(s);
|
||||
@@ -1160,10 +1164,13 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
synchronized (this.loadLock) {
|
||||
if (this.loaded) {
|
||||
try {
|
||||
long startTime = System.nanoTime();
|
||||
this.lastCycleThread = Thread.currentThread().getName();
|
||||
// Run cycle directly instead of scheduling on thread pool
|
||||
// This ensures all cycle tasks in the same tick execute synchronously
|
||||
// preventing wired desync issues
|
||||
this.cycle();
|
||||
this.lastCycleCpuMs = (System.nanoTime() - startTime) / 1000000.0;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
}
|
||||
@@ -1176,8 +1183,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
public void save() {
|
||||
if (this.needsUpdate) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource()
|
||||
.getConnection(); PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) {
|
||||
.getConnection(); PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) {
|
||||
statement.setString(1, this.name);
|
||||
statement.setString(2, this.description);
|
||||
statement.setString(3, this.password);
|
||||
@@ -1245,8 +1252,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
*/
|
||||
public void updateDatabaseUserCount() {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource()
|
||||
.getConnection(); PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE rooms SET users = ? WHERE id = ? LIMIT 1")) {
|
||||
.getConnection(); PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE rooms SET users = ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, this.getUserCount());
|
||||
statement.setInt(2, this.id);
|
||||
statement.executeUpdate();
|
||||
@@ -1486,6 +1493,10 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
return this.getGuildId() != 0;
|
||||
}
|
||||
|
||||
public boolean belongsToGuild() {
|
||||
return this.guild > 0;
|
||||
}
|
||||
|
||||
public void setGuild(int guild) {
|
||||
this.guild = guild;
|
||||
}
|
||||
@@ -1593,7 +1604,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
if (extraData.length == 4) {
|
||||
if (extraData[0].equalsIgnoreCase("1")) {
|
||||
return Color.getHSBColor(Integer.parseInt(extraData[1]),
|
||||
Integer.parseInt(extraData[2]), Integer.parseInt(extraData[3]));
|
||||
Integer.parseInt(extraData[2]), Integer.parseInt(extraData[3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1700,7 +1711,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
|
||||
public String[] filterAnything() {
|
||||
return new String[]{this.getOwnerName(), this.getGuildName(), this.getDescription(),
|
||||
this.getPromotionDesc()};
|
||||
this.getPromotionDesc()};
|
||||
}
|
||||
|
||||
public long getCycleTimestamp() {
|
||||
@@ -1907,7 +1918,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
|
||||
// If the broadcast sender leaves, stop the broadcast for everyone
|
||||
if (!this.youtubeCurrentVideo.isEmpty()
|
||||
&& habbo.getHabboInfo().getUsername().equals(this.youtubeSenderName)) {
|
||||
&& habbo.getHabboInfo().getUsername().equals(this.youtubeSenderName)) {
|
||||
this.clearYoutubeVideo();
|
||||
this.sendComposer(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer("", "", java.util.Collections.emptyList()).compose());
|
||||
}
|
||||
@@ -2052,7 +2063,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
public void talk(final Habbo habbo, final RoomChatMessage roomChatMessage, RoomChatType chatType,
|
||||
boolean ignoreWired) {
|
||||
boolean ignoreWired) {
|
||||
this.chatManager.talk(habbo, roomChatMessage, chatType, ignoreWired);
|
||||
}
|
||||
|
||||
@@ -2197,7 +2208,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
private void loadRights(Connection connection) {
|
||||
this.rights.clear();
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT user_id FROM room_rights WHERE room_id = ?")) {
|
||||
"SELECT user_id FROM room_rights WHERE room_id = ?")) {
|
||||
statement.setInt(1, this.id);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
@@ -2213,7 +2224,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.bannedHabbos.clear();
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT users.username, users.id, room_bans.* FROM room_bans INNER JOIN users ON room_bans.user_id = users.id WHERE ends > ? AND room_bans.room_id = ?")) {
|
||||
"SELECT users.username, users.id, room_bans.* FROM room_bans INNER JOIN users ON room_bans.user_id = users.id WHERE ends > ? AND room_bans.room_id = ?")) {
|
||||
statement.setInt(1, Emulator.getIntUnixTimestamp());
|
||||
statement.setInt(2, this.id);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
@@ -2320,27 +2331,37 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
sanitizedInspectMask |= sanitizedModifyMask;
|
||||
|
||||
synchronized (this.wiredSettingsLock) {
|
||||
int previousInspectMask = this.wiredInspectMask;
|
||||
int previousModifyMask = this.wiredModifyMask;
|
||||
final int finalInspectMask = sanitizedInspectMask;
|
||||
final int finalModifyMask = sanitizedModifyMask;
|
||||
final int finalId = this.id;
|
||||
final int previousInspectMask = this.wiredInspectMask;
|
||||
final int previousModifyMask = this.wiredModifyMask;
|
||||
|
||||
this.wiredInspectMask = sanitizedInspectMask;
|
||||
this.wiredModifyMask = sanitizedModifyMask;
|
||||
this.wiredSettingsLoaded = true;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
|
||||
statement.setInt(1, this.id);
|
||||
statement.setInt(2, sanitizedInspectMask);
|
||||
statement.setInt(3, sanitizedModifyMask);
|
||||
statement.executeUpdate();
|
||||
this.pushWiredSettingsToCurrentHabbos();
|
||||
return true;
|
||||
} catch (SQLException e) {
|
||||
this.wiredInspectMask = previousInspectMask;
|
||||
this.wiredModifyMask = previousModifyMask;
|
||||
LOGGER.error("Caught SQL exception while saving wired room settings", e);
|
||||
return false;
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
|
||||
statement.setInt(1, finalId);
|
||||
statement.setInt(2, finalInspectMask);
|
||||
statement.setInt(3, finalModifyMask);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
synchronized (this.wiredSettingsLock) {
|
||||
if (this.wiredInspectMask == finalInspectMask && this.wiredModifyMask == finalModifyMask) {
|
||||
this.wiredInspectMask = previousInspectMask;
|
||||
this.wiredModifyMask = previousModifyMask;
|
||||
}
|
||||
}
|
||||
LOGGER.error("Caught SQL exception while saving wired room settings", e);
|
||||
}
|
||||
});
|
||||
|
||||
this.pushWiredSettingsToCurrentHabbos();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2413,7 +2434,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT inspect_mask, modify_mask FROM room_wired_settings WHERE room_id = ? LIMIT 1")) {
|
||||
"SELECT inspect_mask, modify_mask FROM room_wired_settings WHERE room_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, this.id);
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
@@ -2513,15 +2534,15 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
if (habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT) || !habbo.getRoomUnit()
|
||||
.canForcePosture()) {
|
||||
.canForcePosture()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dance(habbo, DanceType.NONE);
|
||||
habbo.getRoomUnit().cmdSit = true;
|
||||
habbo.getRoomUnit().setBodyRotation(
|
||||
RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
|
||||
- habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
|
||||
RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
|
||||
- habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
|
||||
habbo.getRoomUnit().setStatus(RoomUnitStatus.SIT, 0.5 + "");
|
||||
this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose());
|
||||
WiredManager.triggerUserPerformsAction(this, habbo.getRoomUnit(), WiredUserActionType.SIT, -1);
|
||||
@@ -2535,11 +2556,11 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
HabboItem item = this.getTopItemAt(habbo.getRoomUnit().getX(), habbo.getRoomUnit().getY());
|
||||
if (item == null || !item.getBaseItem().allowSit() || !item.getBaseItem().allowLay()) {
|
||||
boolean wasSittingOrLaying = habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT)
|
||||
|| habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY);
|
||||
|| habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY);
|
||||
habbo.getRoomUnit().cmdStand = true;
|
||||
habbo.getRoomUnit().setBodyRotation(
|
||||
RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
|
||||
- habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
|
||||
RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
|
||||
- habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
|
||||
habbo.getRoomUnit().removeStatus(RoomUnitStatus.SIT);
|
||||
habbo.getRoomUnit().removeStatus(RoomUnitStatus.LAY);
|
||||
this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose());
|
||||
@@ -2567,38 +2588,38 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
public void updateItem(HabboItem item) {
|
||||
if (this.isLoaded()) {
|
||||
if (item != null && item.getRoomId() == this.id) {
|
||||
if (item.getBaseItem() != null) {
|
||||
if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
|
||||
this.sendComposer(new FloorItemUpdateComposer(item).compose());
|
||||
this.updateTiles(this.getLayout()
|
||||
.getTilesAt(this.layout.getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
|
||||
item.getRotation()));
|
||||
if (this.isLoaded()) {
|
||||
if (item != null && item.getRoomId() == this.id) {
|
||||
if (item.getBaseItem() != null) {
|
||||
if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
|
||||
this.sendComposer(new FloorItemUpdateComposer(item).compose());
|
||||
this.updateTiles(this.getLayout()
|
||||
.getTilesAt(this.layout.getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
|
||||
item.getRotation()));
|
||||
|
||||
if (RoomAreaHideSupport.isControllerItem(item)) {
|
||||
RoomAreaHideSupport.sendState(this, item);
|
||||
}
|
||||
} else if (item.getBaseItem().getType() == FurnitureType.WALL) {
|
||||
this.sendComposer(new WallItemUpdateComposer(item).compose());
|
||||
if (RoomAreaHideSupport.isControllerItem(item)) {
|
||||
RoomAreaHideSupport.sendState(this, item);
|
||||
}
|
||||
} else if (item.getBaseItem().getType() == FurnitureType.WALL) {
|
||||
this.sendComposer(new WallItemUpdateComposer(item).compose());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateItemState(HabboItem item) {
|
||||
if (item != null && RoomAreaHideSupport.isControllerItem(item)) {
|
||||
this.updateItem(item);
|
||||
return;
|
||||
}
|
||||
if (item != null && RoomAreaHideSupport.isControllerItem(item)) {
|
||||
this.updateItem(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.isLimited()) {
|
||||
this.sendComposer(new ItemStateComposer(item).compose());
|
||||
} else {
|
||||
this.sendComposer(new FloorItemUpdateComposer(item).compose());
|
||||
}
|
||||
if (!item.isLimited()) {
|
||||
this.sendComposer(new ItemStateComposer(item).compose());
|
||||
} else {
|
||||
this.sendComposer(new FloorItemUpdateComposer(item).compose());
|
||||
}
|
||||
|
||||
if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
|
||||
if (this.layout == null) {
|
||||
@@ -2606,8 +2627,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
this.updateTiles(this.getLayout()
|
||||
.getTilesAt(this.layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), item.getRotation()));
|
||||
.getTilesAt(this.layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), item.getRotation()));
|
||||
|
||||
if (item instanceof InteractionMultiHeight) {
|
||||
((InteractionMultiHeight) item).updateUnitsOnItem(this);
|
||||
@@ -2615,12 +2636,12 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
if (item.getBaseItem().getType() == FurnitureType.FLOOR
|
||||
&& (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item))) {
|
||||
&& (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item))) {
|
||||
RoomConfInvisSupport.sendState(this);
|
||||
}
|
||||
|
||||
if (item.getBaseItem().getType() == FurnitureType.FLOOR
|
||||
&& RoomHanditemBlockSupport.isControllerItem(item)) {
|
||||
&& RoomHanditemBlockSupport.isControllerItem(item)) {
|
||||
RoomHanditemBlockSupport.sendState(this);
|
||||
}
|
||||
}
|
||||
@@ -2654,18 +2675,18 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
public void refreshGuild(Guild guild) {
|
||||
if (guild.getRoomId() == this.id) {
|
||||
THashSet<GuildMember> members = Emulator.getGameEnvironment().getGuildManager()
|
||||
.getGuildMembers(guild.getId());
|
||||
.getGuildMembers(guild.getId());
|
||||
|
||||
for (Habbo habbo : this.getHabbos()) {
|
||||
Optional<GuildMember> member = members.stream()
|
||||
.filter(m -> m.getUserId() == habbo.getHabboInfo().getId()).findAny();
|
||||
.filter(m -> m.getUserId() == habbo.getHabboInfo().getId()).findAny();
|
||||
|
||||
if (!member.isPresent()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
habbo.getClient()
|
||||
.sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, member.get()));
|
||||
.sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, member.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2700,7 +2721,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
if (habbo.getHabboInfo().getCurrentRoom() == this) {
|
||||
if (habbo.getHabboInfo().getId() != this.ownerId) {
|
||||
if (!(habbo.hasPermission(Permission.ACC_ANYROOMOWNER) || habbo.hasPermission(
|
||||
Permission.ACC_MOVEROTATE))) {
|
||||
Permission.ACC_MOVEROTATE))) {
|
||||
this.refreshRightsForHabbo(habbo);
|
||||
}
|
||||
}
|
||||
@@ -2786,18 +2807,18 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
} else {
|
||||
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
|
||||
this.roomSpecialTypes.getTriggers()).compose());
|
||||
this.roomSpecialTypes.getTriggers()).compose());
|
||||
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
|
||||
this.roomSpecialTypes.getEffects()).compose());
|
||||
this.roomSpecialTypes.getEffects()).compose());
|
||||
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
|
||||
this.roomSpecialTypes.getConditions()).compose());
|
||||
this.roomSpecialTypes.getConditions()).compose());
|
||||
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
|
||||
this.roomSpecialTypes.getExtras()).compose());
|
||||
this.roomSpecialTypes.getExtras()).compose());
|
||||
}
|
||||
}
|
||||
|
||||
public FurnitureMovementError canPlaceFurnitureAt(HabboItem item, Habbo habbo, RoomTile tile,
|
||||
int rotation) {
|
||||
int rotation) {
|
||||
return this.itemManager.canPlaceFurnitureAt(item, habbo, tile, rotation);
|
||||
}
|
||||
|
||||
@@ -2806,17 +2827,17 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
public FurnitureMovementError furnitureFitsAt(RoomTile tile, HabboItem item, int rotation,
|
||||
boolean checkForUnits) {
|
||||
boolean checkForUnits) {
|
||||
return this.itemManager.furnitureFitsAt(tile, item, rotation, checkForUnits);
|
||||
}
|
||||
|
||||
public FurnitureMovementError furnitureFitsAtWithPhysics(RoomTile tile, HabboItem item, int rotation,
|
||||
boolean checkForUnits, WiredMovementPhysics physics) {
|
||||
boolean checkForUnits, WiredMovementPhysics physics) {
|
||||
return this.itemManager.furnitureFitsAtWithPhysics(tile, item, rotation, checkForUnits, physics);
|
||||
}
|
||||
|
||||
public FurnitureMovementError placeFloorFurniAt(HabboItem item, RoomTile tile, int rotation,
|
||||
Habbo owner) {
|
||||
Habbo owner) {
|
||||
return this.itemManager.placeFloorFurniAt(item, tile, rotation, owner);
|
||||
}
|
||||
|
||||
@@ -2825,17 +2846,17 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
|
||||
Habbo actor) {
|
||||
Habbo actor) {
|
||||
return this.itemManager.moveFurniTo(item, tile, rotation, actor);
|
||||
}
|
||||
|
||||
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
|
||||
Habbo actor, boolean sendUpdates) {
|
||||
Habbo actor, boolean sendUpdates) {
|
||||
return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates);
|
||||
}
|
||||
|
||||
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
|
||||
Habbo actor, boolean sendUpdates, boolean checkForUnits) {
|
||||
Habbo actor, boolean sendUpdates, boolean checkForUnits) {
|
||||
return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates, checkForUnits);
|
||||
}
|
||||
|
||||
@@ -2852,12 +2873,12 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation,
|
||||
Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
|
||||
Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
|
||||
return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, actor, sendUpdates, checkForUnits, physics);
|
||||
}
|
||||
|
||||
public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, double z,
|
||||
Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
|
||||
Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
|
||||
return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, z, actor, sendUpdates, checkForUnits, physics);
|
||||
}
|
||||
|
||||
@@ -2878,4 +2899,20 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
public Collection<RoomUnit> getRoomUnitsAt(RoomTile tile) {
|
||||
return this.unitManager.getRoomUnitsAt(tile);
|
||||
}
|
||||
|
||||
public long getEstimatedMemoryUsage() {
|
||||
long bytes = 1024 * 10; // Base footprint
|
||||
if (this.itemManager != null) {
|
||||
bytes += this.itemManager.itemCount() * 512L;
|
||||
}
|
||||
bytes += this.getUserCount() * 2048L;
|
||||
if (this.layout != null) {
|
||||
bytes += this.layout.getMapSize() * 128L;
|
||||
}
|
||||
com.eu.habbo.habbohotel.wired.tick.WiredTickService wired = com.eu.habbo.habbohotel.wired.tick.WiredTickService.getInstance();
|
||||
if (wired != null) {
|
||||
bytes += wired.getTickableCount(this.getId()) * 256L;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,27 +313,6 @@ public class RoomChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
String wiredSayMessage = roomChatMessage.getMessage();
|
||||
|
||||
// Handle commands and wired
|
||||
boolean suppressSaysOutput = false;
|
||||
if (chatType != RoomChatType.WHISPER) {
|
||||
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
|
||||
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
|
||||
roomChatMessage.isCommand = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ignoreWired) {
|
||||
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
|
||||
habbo.getHabboInfo().getCurrentRoom(),
|
||||
habbo.getRoomUnit(),
|
||||
wiredSayMessage,
|
||||
chatType.ordinal(),
|
||||
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Flood protection
|
||||
if (!habbo.hasPermission(Permission.ACC_CHAT_NO_FLOOD)) {
|
||||
final int chatCounter = habbo.getHabboStats().chatCounter.addAndGet(1);
|
||||
@@ -357,6 +336,27 @@ public class RoomChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
String wiredSayMessage = roomChatMessage.getMessage();
|
||||
|
||||
// Handle commands and wired
|
||||
boolean suppressSaysOutput = false;
|
||||
if (chatType != RoomChatType.WHISPER) {
|
||||
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
|
||||
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
|
||||
roomChatMessage.isCommand = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ignoreWired) {
|
||||
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
|
||||
habbo.getHabboInfo().getCurrentRoom(),
|
||||
habbo.getRoomUnit(),
|
||||
wiredSayMessage,
|
||||
chatType.ordinal(),
|
||||
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Build prefix messages
|
||||
ServerMessage prefixMessage = null;
|
||||
|
||||
@@ -615,6 +615,9 @@ public class RoomChatManager {
|
||||
InteractionTalkingFurniture.class);
|
||||
|
||||
for (HabboItem item : items) {
|
||||
if (item.getExtradata().equals("1")) {
|
||||
continue;
|
||||
}
|
||||
if (this.room.getLayout().getTile(item.getX(), item.getY())
|
||||
.distance(habbo.getRoomUnit().getCurrentLocation()) <= Emulator.getConfig()
|
||||
.getInt("furniture.talking.range")) {
|
||||
|
||||
@@ -134,7 +134,18 @@ public class RoomChatMessageBubbles {
|
||||
}
|
||||
|
||||
public static RoomChatMessageBubbles getBubble(int id) {
|
||||
return BUBBLES.getOrDefault(id, NORMAL);
|
||||
RoomChatMessageBubbles bubble = BUBBLES.get(id);
|
||||
if (bubble != null) return bubble;
|
||||
|
||||
// Custom chat bubbles (client-side only, e.g. ids 253+) are not registered
|
||||
// above. Instead of falling back to NORMAL (which made them render as the
|
||||
// default bubble), pass the id through so the server relays it as-is and
|
||||
// the client renders its own .bubble-<id> style. Capped to avoid abuse.
|
||||
if (id > 0 && id <= 1000) {
|
||||
return new RoomChatMessageBubbles(id, "CUSTOM_" + id, "", true, false);
|
||||
}
|
||||
|
||||
return NORMAL;
|
||||
}
|
||||
|
||||
private static void registerBubble(RoomChatMessageBubbles bubble) {
|
||||
|
||||
@@ -75,7 +75,6 @@ public class RoomCycleManager {
|
||||
final boolean[] foundRightHolder = {false};
|
||||
|
||||
boolean loaded = this.room.isLoaded();
|
||||
this.room.tileCache.clear();
|
||||
|
||||
if (loaded) {
|
||||
processScheduledTasks();
|
||||
@@ -164,13 +163,9 @@ public class RoomCycleManager {
|
||||
* Processes scheduled tasks.
|
||||
*/
|
||||
private void processScheduledTasks() {
|
||||
if (!this.room.scheduledTasks.isEmpty()) {
|
||||
Set<Runnable> tasks = this.room.scheduledTasks;
|
||||
this.room.scheduledTasks = ConcurrentHashMap.newKeySet();
|
||||
|
||||
for (Runnable runnable : tasks) {
|
||||
Emulator.getThreading().run(runnable);
|
||||
}
|
||||
Runnable task;
|
||||
while ((task = this.room.scheduledTasks.poll()) != null) {
|
||||
Emulator.getThreading().run(task);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,15 +300,20 @@ public class RoomCycleManager {
|
||||
return;
|
||||
}
|
||||
|
||||
TIntObjectIterator<Bot> botIterator = currentBots.iterator();
|
||||
for (int i = currentBots.size(); i-- > 0; ) {
|
||||
// Snapshot under the map monitor (currentBots is a synchronizedMap whose
|
||||
// iterator isn't concurrency-safe), then cycle OFF-lock. Holding the
|
||||
// monitor across the whole tick would block bot place/pickup and room
|
||||
// dispose for the tick duration AND invert the lock order vs
|
||||
// roomUnitLock -> currentBots taken by RoomUnitManager.addBot/clear.
|
||||
final ArrayList<Bot> bots;
|
||||
synchronized (currentBots) {
|
||||
bots = new ArrayList<>(currentBots.valueCollection());
|
||||
}
|
||||
|
||||
for (Bot bot : bots) {
|
||||
try {
|
||||
final Bot bot;
|
||||
try {
|
||||
botIterator.advance();
|
||||
bot = botIterator.value();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
if (bot == null || bot.getRoomUnit() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.room.isAllowBotsWalk() && bot.getRoomUnit().isWalking()) {
|
||||
@@ -327,10 +327,8 @@ public class RoomCycleManager {
|
||||
if (this.cycleRoomUnit(bot.getRoomUnit(), RoomUnitType.BOT)) {
|
||||
updatedUnit.add(bot.getRoomUnit());
|
||||
}
|
||||
|
||||
} catch (NoSuchElementException e) {
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,31 +342,37 @@ public class RoomCycleManager {
|
||||
return;
|
||||
}
|
||||
|
||||
TIntObjectIterator<Pet> petIterator = currentPets.iterator();
|
||||
for (int i = currentPets.size(); i-- > 0; ) {
|
||||
// Snapshot under the monitor, then cycle off-lock (see processBots): avoids
|
||||
// holding currentPets for the whole tick and the roomUnitLock inversion.
|
||||
final ArrayList<Pet> pets;
|
||||
synchronized (currentPets) {
|
||||
pets = new ArrayList<>(currentPets.valueCollection());
|
||||
}
|
||||
|
||||
for (Pet pet : pets) {
|
||||
try {
|
||||
petIterator.advance();
|
||||
} catch (NoSuchElementException e) {
|
||||
if (pet == null || pet.getRoomUnit() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.cycleRoomUnit(pet.getRoomUnit(), RoomUnitType.PET)) {
|
||||
updatedUnit.add(pet.getRoomUnit());
|
||||
}
|
||||
|
||||
pet.cycle();
|
||||
|
||||
if (pet.packetUpdate) {
|
||||
updatedUnit.add(pet.getRoomUnit());
|
||||
pet.packetUpdate = false;
|
||||
}
|
||||
|
||||
if (pet.getRoomUnit().isWalking() && pet.getRoomUnit().getPath().size() == 1
|
||||
&& pet.getRoomUnit().hasStatus(RoomUnitStatus.GESTURE)) {
|
||||
pet.getRoomUnit().removeStatus(RoomUnitStatus.GESTURE);
|
||||
updatedUnit.add(pet.getRoomUnit());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
break;
|
||||
}
|
||||
|
||||
Pet pet = petIterator.value();
|
||||
if (this.cycleRoomUnit(pet.getRoomUnit(), RoomUnitType.PET)) {
|
||||
updatedUnit.add(pet.getRoomUnit());
|
||||
}
|
||||
|
||||
pet.cycle();
|
||||
|
||||
if (pet.packetUpdate) {
|
||||
updatedUnit.add(pet.getRoomUnit());
|
||||
pet.packetUpdate = false;
|
||||
}
|
||||
|
||||
if (pet.getRoomUnit().isWalking() && pet.getRoomUnit().getPath().size() == 1
|
||||
&& pet.getRoomUnit().hasStatus(RoomUnitStatus.GESTURE)) {
|
||||
pet.getRoomUnit().removeStatus(RoomUnitStatus.GESTURE);
|
||||
updatedUnit.add(pet.getRoomUnit());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,7 +490,7 @@ public class RoomCycleManager {
|
||||
if (!unit.hasStatus(RoomUnitStatus.LAY)) {
|
||||
BedProfile bedProfile = new BedProfile(topItem);
|
||||
double layHeight = Item.getCurrentHeight(topItem) * 1.0D + bedProfile.getLayZOffset();
|
||||
LOGGER.info("[BedProfile] item={} stackHeight={} isFlat={} isDouble={} X={} Y={} Z={}",
|
||||
LOGGER.debug("[BedProfile] item={} stackHeight={} isFlat={} isDouble={} X={} Y={} Z={}",
|
||||
topItem.getBaseItem().getName(), topItem.getBaseItem().getHeight(),
|
||||
bedProfile.isFlat(), bedProfile.isDouble(),
|
||||
bedProfile.getLayXOffset(), bedProfile.getLayYOffset(), bedProfile.getLayZOffset());
|
||||
|
||||
@@ -35,6 +35,7 @@ public class RoomFurniVariableManager {
|
||||
private final Room room;
|
||||
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByFurniId;
|
||||
private volatile boolean permanentAssignmentsLoaded;
|
||||
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
public RoomFurniVariableManager(Room room) {
|
||||
this.room = room;
|
||||
@@ -591,7 +592,22 @@ public class RoomFurniVariableManager {
|
||||
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.createSnapshot(), this.room.getRoomVariableManager().createSnapshot()));
|
||||
}
|
||||
|
||||
public void requestBroadcast() {
|
||||
if (this.broadcastRequested.compareAndSet(false, true)) {
|
||||
Emulator.getThreading().run(() -> {
|
||||
this.broadcastRequested.set(false);
|
||||
if (this.room.isLoaded()) {
|
||||
this.broadcastSnapshotRaw();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastSnapshot() {
|
||||
this.requestBroadcast();
|
||||
}
|
||||
|
||||
public void broadcastSnapshotRaw() {
|
||||
RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot();
|
||||
Snapshot furniSnapshot = this.createSnapshot();
|
||||
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
|
||||
|
||||
@@ -148,55 +148,8 @@ public class RoomItemManager {
|
||||
item = this.roomItems.get(id);
|
||||
}
|
||||
|
||||
// Check special types if not found in main storage
|
||||
RoomSpecialTypes specialTypes = this.room.getRoomSpecialTypes();
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getBanzaiTeleporter(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getTrigger(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getEffect(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getCondition(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getGameGate(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getGameScorebord(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getGameTimer(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getFreezeExitTiles().get(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getRoller(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getNest(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getPetDrink(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getPetFood(id);
|
||||
item = this.room.getRoomSpecialTypes().getSpecialItem(id);
|
||||
}
|
||||
|
||||
return item;
|
||||
@@ -214,17 +167,22 @@ public class RoomItemManager {
|
||||
*/
|
||||
public THashSet<HabboItem> getFloorItems() {
|
||||
THashSet<HabboItem> items = new THashSet<>();
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
// roomItems is a TCollections.synchronizedMap; its iterator is not safe
|
||||
// against concurrent put/remove (item place/pickup), so hold the map
|
||||
// monitor for the whole traversal, matching the mutation sites.
|
||||
synchronized (this.roomItems) {
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
try {
|
||||
iterator.advance();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
try {
|
||||
iterator.advance();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (iterator.value().getBaseItem().getType() == FurnitureType.FLOOR) {
|
||||
items.add(iterator.value());
|
||||
if (iterator.value().getBaseItem().getType() == FurnitureType.FLOOR) {
|
||||
items.add(iterator.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,17 +194,19 @@ public class RoomItemManager {
|
||||
*/
|
||||
public THashSet<HabboItem> getWallItems() {
|
||||
THashSet<HabboItem> items = new THashSet<>();
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
synchronized (this.roomItems) {
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
try {
|
||||
iterator.advance();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
try {
|
||||
iterator.advance();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (iterator.value().getBaseItem().getType() == FurnitureType.WALL) {
|
||||
items.add(iterator.value());
|
||||
if (iterator.value().getBaseItem().getType() == FurnitureType.WALL) {
|
||||
items.add(iterator.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,18 +218,20 @@ public class RoomItemManager {
|
||||
*/
|
||||
public THashSet<HabboItem> getPostItNotes() {
|
||||
THashSet<HabboItem> items = new THashSet<>();
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
synchronized (this.roomItems) {
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
try {
|
||||
iterator.advance();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
try {
|
||||
iterator.advance();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (iterator.value().getBaseItem().getInteractionType().getType()
|
||||
== InteractionPostIt.class) {
|
||||
items.add(iterator.value());
|
||||
if (iterator.value().getBaseItem().getInteractionType().getType()
|
||||
== InteractionPostIt.class) {
|
||||
items.add(iterator.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,44 +285,49 @@ public class RoomItemManager {
|
||||
}
|
||||
}
|
||||
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
// Cache miss: iterate roomItems under its monitor so a concurrent
|
||||
// place/pickup can't rehash the map mid-traversal (which the per-advance
|
||||
// try/catch would otherwise silently swallow into an incomplete result).
|
||||
synchronized (this.roomItems) {
|
||||
TIntObjectIterator<HabboItem> iterator = this.roomItems.iterator();
|
||||
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
HabboItem item;
|
||||
try {
|
||||
iterator.advance();
|
||||
item = iterator.value();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
for (int i = this.roomItems.size(); i-- > 0; ) {
|
||||
HabboItem item;
|
||||
try {
|
||||
iterator.advance();
|
||||
item = iterator.value();
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.getBaseItem().getType() != FurnitureType.FLOOR) {
|
||||
continue;
|
||||
}
|
||||
if (item.getBaseItem().getType() != FurnitureType.FLOOR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int width, length;
|
||||
int width, length;
|
||||
|
||||
if (item.getRotation() != 2 && item.getRotation() != 6) {
|
||||
width = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
|
||||
length = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
|
||||
} else {
|
||||
width = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
|
||||
length = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
|
||||
}
|
||||
if (item.getRotation() != 2 && item.getRotation() != 6) {
|
||||
width = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
|
||||
length = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
|
||||
} else {
|
||||
width = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
|
||||
length = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
|
||||
}
|
||||
|
||||
if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY()
|
||||
&& tile.y <= item.getY() + length - 1)) {
|
||||
continue;
|
||||
}
|
||||
if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY()
|
||||
&& tile.y <= item.getY() + length - 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.add(item);
|
||||
items.add(item);
|
||||
|
||||
if (returnOnFirst) {
|
||||
return items;
|
||||
if (returnOnFirst) {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +693,7 @@ public class RoomItemManager {
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope ||
|
||||
item instanceof InteractionFireworks) {
|
||||
item instanceof InteractionFireworks || item instanceof InteractionVoteCounter) {
|
||||
specialTypes.addUndefined(item);
|
||||
}
|
||||
}
|
||||
@@ -899,7 +866,7 @@ public class RoomItemManager {
|
||||
item instanceof InteractionStickyPole ||
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope) {
|
||||
item instanceof InteractionSnowboardSlope || item instanceof InteractionVoteCounter) {
|
||||
specialTypes.removeUndefined(item);
|
||||
}
|
||||
|
||||
@@ -1003,9 +970,11 @@ public class RoomItemManager {
|
||||
public int getUserUniqueFurniCount(int userId) {
|
||||
THashSet<Item> items = new THashSet<>();
|
||||
|
||||
for (HabboItem item : this.roomItems.valueCollection()) {
|
||||
if (!items.contains(item.getBaseItem()) && item.getUserId() == userId) {
|
||||
items.add(item.getBaseItem());
|
||||
synchronized (this.roomItems) {
|
||||
for (HabboItem item : this.roomItems.valueCollection()) {
|
||||
if (!items.contains(item.getBaseItem()) && item.getUserId() == userId) {
|
||||
items.add(item.getBaseItem());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,13 +130,16 @@ public class RoomLayout {
|
||||
this.roomTiles = new RoomTile[this.mapSizeX][this.mapSizeY];
|
||||
|
||||
for (short y = 0; y < this.mapSizeY; y++) {
|
||||
if (modelTemp[y].isEmpty() || modelTemp[y].equalsIgnoreCase("\r")) {
|
||||
continue;
|
||||
}
|
||||
// A row shorter/longer than the model width (or empty) cannot be parsed
|
||||
// per-square. Previously such tiles were left null while tileExists()
|
||||
// still reported them present, causing NPEs in the coordinate accessors.
|
||||
// Fill them with INVALID tiles so every in-bounds coordinate is non-null.
|
||||
boolean validRow = !modelTemp[y].isEmpty() && modelTemp[y].length() == this.mapSizeX;
|
||||
|
||||
for (short x = 0; x < this.mapSizeX; x++) {
|
||||
if (modelTemp[y].length() != this.mapSizeX) {
|
||||
break;
|
||||
if (!validRow) {
|
||||
this.roomTiles[x][y] = new RoomTile(x, y, (short) 0, RoomTileState.INVALID, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
String square = modelTemp[y].substring(x, x + 1).trim().toLowerCase();
|
||||
@@ -159,7 +162,9 @@ public class RoomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
this.doorTile = this.roomTiles[this.doorX][this.doorY];
|
||||
this.doorTile = (this.doorX >= 0 && this.doorX < this.mapSizeX && this.doorY >= 0 && this.doorY < this.mapSizeY)
|
||||
? this.roomTiles[this.doorX][this.doorY]
|
||||
: null;
|
||||
|
||||
if (this.doorTile != null) {
|
||||
this.doorTile.setAllowStack(false);
|
||||
|
||||
@@ -731,10 +731,10 @@ public class RoomManager {
|
||||
|
||||
habbo.getClient().sendResponse(new RoomModelComposer(room));
|
||||
|
||||
if (!room.getWallPaint().equals("0.0"))
|
||||
if (room.getWallPaint() != null && !room.getWallPaint().equals("0.0"))
|
||||
habbo.getClient().sendResponse(new RoomPaintComposer("wallpaper", room.getWallPaint()));
|
||||
|
||||
if (!room.getFloorPaint().equals("0.0"))
|
||||
if (room.getFloorPaint() != null && !room.getFloorPaint().equals("0.0"))
|
||||
habbo.getClient().sendResponse(new RoomPaintComposer("floor", room.getFloorPaint()));
|
||||
|
||||
habbo.getClient().sendResponse(new RoomPaintComposer("landscape", room.getBackgroundPaint()));
|
||||
@@ -1020,6 +1020,10 @@ public class RoomManager {
|
||||
room.getYoutubeWatchers()).compose());
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer(
|
||||
room.isSoundboardEnabled(),
|
||||
Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose());
|
||||
|
||||
WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit());
|
||||
room.habboEntered(habbo);
|
||||
|
||||
|
||||
@@ -272,10 +272,16 @@ public class RoomRightsManager {
|
||||
} else if (this.isOwner(habbo)) {
|
||||
habbo.getClient().sendResponse(new RoomOwnerComposer());
|
||||
flatCtrl = RoomRightLevels.MODERATOR;
|
||||
} else if (this.hasRights(habbo) && !this.room.hasGuild()) {
|
||||
flatCtrl = RoomRightLevels.RIGHTS;
|
||||
} else if (this.room.hasGuild()) {
|
||||
flatCtrl = this.getGuildRightLevel(habbo);
|
||||
// Explicit room rights must still be honoured in guild rooms (the old
|
||||
// `&& !hasGuild()` guard stripped them for non-guild members) — take
|
||||
// whichever of the two is stronger.
|
||||
RoomRightLevels guildLevel = this.getGuildRightLevel(habbo);
|
||||
flatCtrl = (this.hasRights(habbo) && RoomRightLevels.RIGHTS.isEqualOrGreaterThan(guildLevel))
|
||||
? RoomRightLevels.RIGHTS
|
||||
: guildLevel;
|
||||
} else if (this.hasRights(habbo)) {
|
||||
flatCtrl = RoomRightLevels.RIGHTS;
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new RoomRightsComposer(flatCtrl));
|
||||
|
||||
@@ -71,6 +71,7 @@ public class RoomSpecialTypes {
|
||||
private final THashMap<Integer, InteractionFreezeExitTile> freezeExitTile;
|
||||
private final THashMap<Integer, HabboItem> undefined;
|
||||
private final Set<ICycleable> cycleTasks;
|
||||
private final ConcurrentHashMap<Integer, HabboItem> specialItemsById = new ConcurrentHashMap<>();
|
||||
|
||||
public RoomSpecialTypes() {
|
||||
this.banzaiTeleporters = new THashMap<>(0);
|
||||
@@ -115,11 +116,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) {
|
||||
this.banzaiTeleporters.put(item.getId(), item);
|
||||
this.banzaiTeleporters.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) {
|
||||
this.banzaiTeleporters.remove(item.getId());
|
||||
this.banzaiTeleporters.remove(item.getId()); this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionBattleBanzaiTeleporter> getBanzaiTeleporters() {
|
||||
@@ -151,15 +152,23 @@ public class RoomSpecialTypes {
|
||||
|
||||
|
||||
public InteractionNest getNest(int itemId) {
|
||||
return this.nests.get(itemId);
|
||||
synchronized (this.nests) {
|
||||
return this.nests.get(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
public void addNest(InteractionNest item) {
|
||||
this.nests.put(item.getId(), item);
|
||||
synchronized (this.nests) {
|
||||
this.nests.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeNest(InteractionNest item) {
|
||||
this.nests.remove(item.getId());
|
||||
synchronized (this.nests) {
|
||||
this.nests.remove(item.getId());
|
||||
}
|
||||
this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionNest> getNests() {
|
||||
@@ -173,15 +182,23 @@ public class RoomSpecialTypes {
|
||||
|
||||
|
||||
public InteractionPetDrink getPetDrink(int itemId) {
|
||||
return this.petDrinks.get(itemId);
|
||||
synchronized (this.petDrinks) {
|
||||
return this.petDrinks.get(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
public void addPetDrink(InteractionPetDrink item) {
|
||||
this.petDrinks.put(item.getId(), item);
|
||||
synchronized (this.petDrinks) {
|
||||
this.petDrinks.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetDrink(InteractionPetDrink item) {
|
||||
this.petDrinks.remove(item.getId());
|
||||
synchronized (this.petDrinks) {
|
||||
this.petDrinks.remove(item.getId());
|
||||
}
|
||||
this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetDrink> getPetDrinks() {
|
||||
@@ -195,15 +212,23 @@ public class RoomSpecialTypes {
|
||||
|
||||
|
||||
public InteractionPetFood getPetFood(int itemId) {
|
||||
return this.petFoods.get(itemId);
|
||||
synchronized (this.petFoods) {
|
||||
return this.petFoods.get(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
public void addPetFood(InteractionPetFood item) {
|
||||
this.petFoods.put(item.getId(), item);
|
||||
synchronized (this.petFoods) {
|
||||
this.petFoods.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetFood(InteractionPetFood petFood) {
|
||||
this.petFoods.remove(petFood.getId());
|
||||
synchronized (this.petFoods) {
|
||||
this.petFoods.remove(petFood.getId());
|
||||
}
|
||||
this.specialItemsById.remove(petFood.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetFood> getPetFoods() {
|
||||
@@ -217,15 +242,23 @@ public class RoomSpecialTypes {
|
||||
|
||||
|
||||
public InteractionPetToy getPetToy(int itemId) {
|
||||
return this.petToys.get(itemId);
|
||||
synchronized (this.petToys) {
|
||||
return this.petToys.get(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
public void addPetToy(InteractionPetToy item) {
|
||||
this.petToys.put(item.getId(), item);
|
||||
synchronized (this.petToys) {
|
||||
this.petToys.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetToy(InteractionPetToy petToy) {
|
||||
this.petToys.remove(petToy.getId());
|
||||
synchronized (this.petToys) {
|
||||
this.petToys.remove(petToy.getId());
|
||||
}
|
||||
this.specialItemsById.remove(petToy.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetToy> getPetToys() {
|
||||
@@ -239,15 +272,23 @@ public class RoomSpecialTypes {
|
||||
|
||||
|
||||
public InteractionPetTree getPetTree(int itemId) {
|
||||
return this.petTrees.get(itemId);
|
||||
synchronized (this.petTrees) {
|
||||
return this.petTrees.get(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
public void addPetTree(InteractionPetTree item) {
|
||||
this.petTrees.put(item.getId(), item);
|
||||
synchronized (this.petTrees) {
|
||||
this.petTrees.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetTree(InteractionPetTree petTree) {
|
||||
this.petTrees.remove(petTree.getId());
|
||||
synchronized (this.petTrees) {
|
||||
this.petTrees.remove(petTree.getId());
|
||||
}
|
||||
this.specialItemsById.remove(petTree.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetTree> getPetTrees() {
|
||||
@@ -270,12 +311,14 @@ public class RoomSpecialTypes {
|
||||
synchronized (this.rollers) {
|
||||
this.rollers.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeRoller(InteractionRoller roller) {
|
||||
synchronized (this.rollers) {
|
||||
this.rollers.remove(roller.getId());
|
||||
}
|
||||
this.specialItemsById.remove(roller.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionRoller> getRollers() {
|
||||
@@ -469,11 +512,11 @@ public class RoomSpecialTypes {
|
||||
// Add to type-based index
|
||||
this.wiredTriggers.computeIfAbsent(trigger.getType(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(trigger);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(trigger.getX(), trigger.getY());
|
||||
this.wiredTriggersByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(trigger);
|
||||
this.specialItemsById.put(trigger.getId(), trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,7 +532,6 @@ public class RoomSpecialTypes {
|
||||
this.wiredTriggers.remove(trigger.getType());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(trigger.getX(), trigger.getY());
|
||||
Set<InteractionWiredTrigger> locationTriggers = this.wiredTriggersByLocation.get(key);
|
||||
@@ -499,6 +541,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredTriggersByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(trigger.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -589,11 +632,11 @@ public class RoomSpecialTypes {
|
||||
// Add to type-based index
|
||||
this.wiredEffects.computeIfAbsent(effect.getType(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(effect);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(effect.getX(), effect.getY());
|
||||
this.wiredEffectsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(effect);
|
||||
this.specialItemsById.put(effect.getId(), effect);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -609,7 +652,6 @@ public class RoomSpecialTypes {
|
||||
this.wiredEffects.remove(effect.getType());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(effect.getX(), effect.getY());
|
||||
Set<InteractionWiredEffect> locationEffects = this.wiredEffectsByLocation.get(key);
|
||||
@@ -619,6 +661,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredEffectsByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(effect.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -709,11 +752,11 @@ public class RoomSpecialTypes {
|
||||
// Add to type-based index
|
||||
this.wiredConditions.computeIfAbsent(condition.getType(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(condition);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(condition.getX(), condition.getY());
|
||||
this.wiredConditionsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(condition);
|
||||
this.specialItemsById.put(condition.getId(), condition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -729,7 +772,6 @@ public class RoomSpecialTypes {
|
||||
this.wiredConditions.remove(condition.getType());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(condition.getX(), condition.getY());
|
||||
Set<InteractionWiredCondition> locationConditions = this.wiredConditionsByLocation.get(key);
|
||||
@@ -739,6 +781,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredConditionsByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(condition.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -805,11 +848,11 @@ public class RoomSpecialTypes {
|
||||
*/
|
||||
public void addExtra(InteractionWiredExtra extra) {
|
||||
this.wiredExtras.put(extra.getId(), extra);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(extra.getX(), extra.getY());
|
||||
this.wiredExtrasByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(extra);
|
||||
this.specialItemsById.put(extra.getId(), extra);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -818,7 +861,6 @@ public class RoomSpecialTypes {
|
||||
*/
|
||||
public void removeExtra(InteractionWiredExtra extra) {
|
||||
this.wiredExtras.remove(extra.getId());
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(extra.getX(), extra.getY());
|
||||
Set<InteractionWiredExtra> locationExtras = this.wiredExtrasByLocation.get(key);
|
||||
@@ -828,6 +870,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredExtrasByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(extra.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -880,11 +923,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addGameScoreboard(InteractionGameScoreboard scoreboard) {
|
||||
this.gameScoreboards.put(scoreboard.getId(), scoreboard);
|
||||
this.gameScoreboards.put(scoreboard.getId(), scoreboard); this.specialItemsById.put(scoreboard.getId(), scoreboard);
|
||||
}
|
||||
|
||||
public void removeScoreboard(InteractionGameScoreboard scoreboard) {
|
||||
this.gameScoreboards.remove(scoreboard.getId());
|
||||
this.gameScoreboards.remove(scoreboard.getId()); this.specialItemsById.remove(scoreboard.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionFreezeScoreboard> getFreezeScoreboards() {
|
||||
@@ -980,11 +1023,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addGameGate(InteractionGameGate gameGate) {
|
||||
this.gameGates.put(gameGate.getId(), gameGate);
|
||||
this.gameGates.put(gameGate.getId(), gameGate); this.specialItemsById.put(gameGate.getId(), gameGate);
|
||||
}
|
||||
|
||||
public void removeGameGate(InteractionGameGate gameGate) {
|
||||
this.gameGates.remove(gameGate.getId());
|
||||
this.gameGates.remove(gameGate.getId()); this.specialItemsById.remove(gameGate.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionFreezeGate> getFreezeGates() {
|
||||
@@ -1021,11 +1064,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addGameTimer(InteractionGameTimer gameTimer) {
|
||||
this.gameTimers.put(gameTimer.getId(), gameTimer);
|
||||
this.gameTimers.put(gameTimer.getId(), gameTimer); this.specialItemsById.put(gameTimer.getId(), gameTimer);
|
||||
}
|
||||
|
||||
public void removeGameTimer(InteractionGameTimer gameTimer) {
|
||||
this.gameTimers.remove(gameTimer.getId());
|
||||
this.gameTimers.remove(gameTimer.getId()); this.specialItemsById.remove(gameTimer.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionGameTimer> getGameTimers() {
|
||||
@@ -1043,7 +1086,7 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addFreezeExitTile(InteractionFreezeExitTile freezeExitTile) {
|
||||
this.freezeExitTile.put(freezeExitTile.getId(), freezeExitTile);
|
||||
this.freezeExitTile.put(freezeExitTile.getId(), freezeExitTile); this.specialItemsById.put(freezeExitTile.getId(), freezeExitTile);
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionFreezeExitTile> getFreezeExitTiles() {
|
||||
@@ -1051,7 +1094,7 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void removeFreezeExitTile(InteractionFreezeExitTile freezeExitTile) {
|
||||
this.freezeExitTile.remove(freezeExitTile.getId());
|
||||
this.freezeExitTile.remove(freezeExitTile.getId()); this.specialItemsById.remove(freezeExitTile.getId());
|
||||
}
|
||||
|
||||
public boolean hasFreezeExitTile() {
|
||||
@@ -1062,12 +1105,14 @@ public class RoomSpecialTypes {
|
||||
synchronized (this.undefined) {
|
||||
this.undefined.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeUndefined(HabboItem item) {
|
||||
synchronized (this.undefined) {
|
||||
this.undefined.remove(item.getId());
|
||||
}
|
||||
this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<HabboItem> getItemsOfType(Class<? extends HabboItem> type) {
|
||||
@@ -1130,6 +1175,10 @@ public class RoomSpecialTypes {
|
||||
this.cycleTasks.remove(task);
|
||||
}
|
||||
|
||||
public HabboItem getSpecialItem(int itemId) {
|
||||
return this.specialItemsById.get(itemId);
|
||||
}
|
||||
|
||||
public synchronized void dispose() {
|
||||
this.banzaiTeleporters.clear();
|
||||
this.nests.clear();
|
||||
@@ -1142,6 +1191,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredTriggers.clear();
|
||||
this.wiredEffects.clear();
|
||||
this.wiredConditions.clear();
|
||||
this.wiredExtras.clear();
|
||||
|
||||
this.gameScoreboards.clear();
|
||||
this.gameGates.clear();
|
||||
@@ -1150,6 +1200,7 @@ public class RoomSpecialTypes {
|
||||
this.freezeExitTile.clear();
|
||||
this.undefined.clear();
|
||||
this.cycleTasks.clear();
|
||||
this.specialItemsById.clear();
|
||||
}
|
||||
|
||||
public Rectangle tentAt(RoomTile location) {
|
||||
|
||||
@@ -29,7 +29,6 @@ public class RoomTileManager {
|
||||
*/
|
||||
public void updateTile(RoomTile tile) {
|
||||
if (tile != null) {
|
||||
this.room.tileCache.remove(tile);
|
||||
this.room.getItemManager().tileCache.remove(tile);
|
||||
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
|
||||
tile.setState(this.calculateTileState(tile));
|
||||
@@ -41,7 +40,6 @@ public class RoomTileManager {
|
||||
*/
|
||||
public void updateTiles(THashSet<RoomTile> tiles) {
|
||||
for (RoomTile tile : tiles) {
|
||||
this.room.tileCache.remove(tile);
|
||||
this.room.getItemManager().tileCache.remove(tile);
|
||||
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
|
||||
tile.setState(this.calculateTileState(tile));
|
||||
|
||||
@@ -26,6 +26,7 @@ public class RoomTrade {
|
||||
|
||||
private final List<RoomTradeUser> users;
|
||||
private final Room room;
|
||||
private boolean completed = false;
|
||||
|
||||
public RoomTrade(Habbo userOne, Habbo userTwo, Room room) {
|
||||
this.users = new ArrayList<>();
|
||||
@@ -54,7 +55,7 @@ public class RoomTrade {
|
||||
this.sendMessageToUsers(new TradeStartComposer(this));
|
||||
}
|
||||
|
||||
public void offerItem(Habbo habbo, HabboItem item) {
|
||||
public synchronized void offerItem(Habbo habbo, HabboItem item) {
|
||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||
|
||||
if (user.getItems().contains(item))
|
||||
@@ -67,7 +68,7 @@ public class RoomTrade {
|
||||
this.updateWindow();
|
||||
}
|
||||
|
||||
public void offerMultipleItems(Habbo habbo, THashSet<HabboItem> items) {
|
||||
public synchronized void offerMultipleItems(Habbo habbo, THashSet<HabboItem> items) {
|
||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||
|
||||
for (HabboItem item : items) {
|
||||
@@ -81,7 +82,7 @@ public class RoomTrade {
|
||||
this.updateWindow();
|
||||
}
|
||||
|
||||
public void removeItem(Habbo habbo, HabboItem item) {
|
||||
public synchronized void removeItem(Habbo habbo, HabboItem item) {
|
||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||
|
||||
if (!user.getItems().contains(item))
|
||||
@@ -94,7 +95,7 @@ public class RoomTrade {
|
||||
this.updateWindow();
|
||||
}
|
||||
|
||||
public void accept(Habbo habbo, boolean value) {
|
||||
public synchronized void accept(Habbo habbo, boolean value) {
|
||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||
|
||||
user.setAccepted(value);
|
||||
@@ -110,7 +111,13 @@ public class RoomTrade {
|
||||
}
|
||||
}
|
||||
|
||||
public void confirm(Habbo habbo) {
|
||||
public synchronized void confirm(Habbo habbo) {
|
||||
// Re-entry guard: both participants confirm on their own EventLoop
|
||||
// threads. Without this (and the method-level lock) two concurrent
|
||||
// confirms could each observe "all confirmed" and run tradeItems()
|
||||
// twice → item/credit duplication.
|
||||
if (this.completed) return;
|
||||
|
||||
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
|
||||
|
||||
user.confirm();
|
||||
@@ -122,6 +129,8 @@ public class RoomTrade {
|
||||
accepted = false;
|
||||
}
|
||||
if (accepted) {
|
||||
this.completed = true;
|
||||
|
||||
if (this.tradeItems()) {
|
||||
this.closeWindow();
|
||||
this.sendMessageToUsers(new TradeCompleteComposer());
|
||||
@@ -264,6 +273,10 @@ public class RoomTrade {
|
||||
protected void clearAccepted() {
|
||||
for (RoomTradeUser user : this.users) {
|
||||
user.setAccepted(false);
|
||||
// Any change to the offered items invalidates a prior confirmation;
|
||||
// without this a stale confirmed=true lets a user strip their side
|
||||
// and still complete the trade once the partner re-confirms.
|
||||
user.setConfirmed(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ public class RoomTradeUser {
|
||||
this.confirmed = true;
|
||||
}
|
||||
|
||||
public void setConfirmed(boolean value) {
|
||||
this.confirmed = value;
|
||||
}
|
||||
|
||||
public void addItem(HabboItem item) {
|
||||
this.items.add(item);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -71,7 +72,10 @@ public class RoomUnit {
|
||||
private RoomUserRotation headRotation = RoomUserRotation.NORTH;
|
||||
private DanceType danceType;
|
||||
private RoomUnitType roomUnitType;
|
||||
private Deque<RoomTile> path = new LinkedList<>();
|
||||
// Concurrent + volatile: the room cycle thread polls/clears this path while a
|
||||
// walk packet thread rebuilds it via findPath/setPath. A plain LinkedList would
|
||||
// corrupt under the concurrent structural modification.
|
||||
private volatile Deque<RoomTile> path = new ConcurrentLinkedDeque<>();
|
||||
private int handItem;
|
||||
private long handItemTimestamp;
|
||||
private long lastRollerTime;
|
||||
@@ -587,7 +591,7 @@ public class RoomUnit {
|
||||
Deque<RoomTile> newPath = this.room.getLayout().getPathfinder()
|
||||
.findPath(this.currentLocation, this.goalLocation, this.goalLocation, this);
|
||||
if (newPath != null && !newPath.isEmpty()) {
|
||||
this.path = newPath;
|
||||
this.path = new ConcurrentLinkedDeque<>(newPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,7 +769,7 @@ public class RoomUnit {
|
||||
}
|
||||
|
||||
public void setPath(Deque<RoomTile> path) {
|
||||
this.path = path;
|
||||
this.path = (path == null) ? new ConcurrentLinkedDeque<>() : new ConcurrentLinkedDeque<>(path);
|
||||
}
|
||||
|
||||
public RoomRightLevels getRightsLevel() {
|
||||
|
||||
@@ -71,6 +71,24 @@ public class RoomUnitManager {
|
||||
*/
|
||||
public void clear() {
|
||||
synchronized (this.room.roomUnitLock) {
|
||||
for (Habbo habbo : this.currentHabbos.values()) {
|
||||
if (habbo.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Bot bot : this.currentBots.valueCollection()) {
|
||||
if (bot.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Pet pet : this.currentPets.valueCollection()) {
|
||||
if (pet.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
}
|
||||
}
|
||||
this.unitCounter = 0;
|
||||
this.currentHabbos.clear();
|
||||
this.currentPets.clear();
|
||||
@@ -222,6 +240,8 @@ public class RoomUnitManager {
|
||||
}
|
||||
|
||||
if (habbo.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredManager.triggerUserLeavesRoom(this.room, habbo.getRoomUnit());
|
||||
if (WiredFreezeUtil.isFrozen(habbo.getRoomUnit())) {
|
||||
WiredFreezeUtil.unfreeze(this.room, habbo.getRoomUnit());
|
||||
@@ -646,14 +666,22 @@ public class RoomUnitManager {
|
||||
public boolean removeBot(Bot bot) {
|
||||
synchronized (this.currentBots) {
|
||||
if (this.currentBots.containsKey(bot.getId())) {
|
||||
if (bot.getRoomUnit() != null && bot.getRoomUnit().getCurrentLocation() != null) {
|
||||
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit());
|
||||
if (bot.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
if (bot.getRoomUnit().getCurrentLocation() != null) {
|
||||
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit());
|
||||
}
|
||||
}
|
||||
|
||||
this.currentBots.remove(bot.getId());
|
||||
bot.getRoomUnit().setInRoom(false);
|
||||
if (bot.getRoomUnit() != null) {
|
||||
bot.getRoomUnit().setInRoom(false);
|
||||
}
|
||||
bot.setRoom(null);
|
||||
this.room.sendComposer(new RoomUserRemoveComposer(bot.getRoomUnit()).compose());
|
||||
if (bot.getRoomUnit() != null) {
|
||||
this.room.sendComposer(new RoomUserRemoveComposer(bot.getRoomUnit()).compose());
|
||||
}
|
||||
bot.setRoomUnit(null);
|
||||
return true;
|
||||
}
|
||||
@@ -876,7 +904,12 @@ public class RoomUnitManager {
|
||||
* Removes a Pet from the room.
|
||||
*/
|
||||
public Pet removePet(int petId) {
|
||||
return this.currentPets.remove(petId);
|
||||
Pet pet = this.currentPets.remove(petId);
|
||||
if (pet != null && pet.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
}
|
||||
return pet;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1454,6 +1487,24 @@ public class RoomUnitManager {
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
for (Habbo habbo : this.currentHabbos.values()) {
|
||||
if (habbo.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Bot bot : this.currentBots.valueCollection()) {
|
||||
if (bot.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Pet pet : this.currentPets.valueCollection()) {
|
||||
if (pet.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
}
|
||||
}
|
||||
this.currentHabbos.clear();
|
||||
this.currentBots.clear();
|
||||
this.currentPets.clear();
|
||||
|
||||
@@ -35,6 +35,7 @@ public class RoomUserVariableManager {
|
||||
|
||||
private final Room room;
|
||||
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByUserId;
|
||||
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
public RoomUserVariableManager(Room room) {
|
||||
this.room = room;
|
||||
@@ -660,7 +661,22 @@ public class RoomUserVariableManager {
|
||||
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.room.getRoomVariableManager().createSnapshot()));
|
||||
}
|
||||
|
||||
public void requestBroadcast() {
|
||||
if (this.broadcastRequested.compareAndSet(false, true)) {
|
||||
Emulator.getThreading().run(() -> {
|
||||
this.broadcastRequested.set(false);
|
||||
if (this.room.isLoaded()) {
|
||||
this.broadcastSnapshotRaw();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastSnapshot() {
|
||||
this.requestBroadcast();
|
||||
}
|
||||
|
||||
public void broadcastSnapshotRaw() {
|
||||
Snapshot userSnapshot = this.createSnapshot();
|
||||
RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot();
|
||||
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user