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
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 510e0d082e | |||
| e13c7fdbb6 | |||
| 2a28fbd2e5 | |||
| cd60cba355 | |||
| e62f461962 | |||
| 7f8c98e4f3 | |||
| d95e09e64f | |||
| ebe0690e46 | |||
| 0dda0ae0f7 | |||
| 54ab6613f1 | |||
| 9fda766ba5 | |||
| 3da9325344 | |||
| 770739c256 | |||
| 3ec468993a | |||
| 0e0f1cbb15 | |||
| daeda761cd | |||
| 0906048a3a | |||
| 19cde45d3e | |||
| 8161e3d7e5 | |||
| 5c0f2d2855 | |||
| d984461cc0 | |||
| 61ea33ac28 | |||
| b6ee400b83 | |||
| 62104596ac | |||
| fad6be158a | |||
| a9f1903465 | |||
| af82352f24 | |||
| dcc23ba744 | |||
| f7556138aa | |||
| a0910d822c | |||
| 4eb1484daf | |||
| 45d01876c1 | |||
| 1c4449fb88 | |||
| 373d0399c1 | |||
| 01c17c0511 | |||
| d1570d3574 | |||
| c98d3a3205 | |||
| da1fd01074 | |||
| f7bd452cb0 | |||
| 48fcd3f78b | |||
| 1275254fa0 | |||
| 7ed7a1ec5a | |||
| d383c43bbf | |||
| 44bfcc49b4 | |||
| b0ffb64cb2 | |||
| 1f4eef8e2e | |||
| bfc6ff21a5 | |||
| ea88934e9e | |||
| bb4b9fb7f4 | |||
| 84d7968b76 | |||
| f5bf4baa79 | |||
| 4a02d22061 | |||
| 14854efaeb | |||
| 564c8d647e | |||
| 0e7138a721 | |||
| 76eb1ecd05 | |||
| 4621ed62b7 | |||
| 2b8ce3cd91 | |||
| 57c36da795 | |||
| 17629c210c | |||
| 50444003bb | |||
| f55b182d8e | |||
| 1416cd7464 | |||
| 392d24b9c5 | |||
| 9dcd58d027 | |||
| 3b85d5fa34 | |||
| 43c2c2b0f1 | |||
| a815c1b99d | |||
| caf6ad35fa | |||
| 258a95a269 | |||
| 4944d41410 | |||
| 8fb117ae73 | |||
| 7f4f7d6da9 | |||
| 0cf46471f2 | |||
| 3a505cd559 | |||
| f2e0f6e2d5 | |||
| d73573e7c5 | |||
| efb88e5957 | |||
| e7e75a285b | |||
| 28c3e93945 | |||
| 5bf1d42cfb | |||
| b162b3f4d8 | |||
| 86498b6b4c | |||
| 964f388594 | |||
| f9644d83b7 | |||
| 0b142d184c | |||
| 867c8ff857 | |||
| 5094d6ce4f | |||
| 2c0ef9873c | |||
| dadc1b8aaf | |||
| 85758b53fa | |||
| 2171b5f2ec | |||
| 46306c8205 | |||
| fadec887cd | |||
| e614c1d64f | |||
| e7deea7d9d | |||
| 44ea3abd4e | |||
| 609cd20ab2 | |||
| 717a7f184f | |||
| 2862446686 | |||
| e97e680006 | |||
| 9f36d95dbc |
@@ -1,26 +1,89 @@
|
||||
CREATE TABLE IF NOT EXISTS `habbo_mentions` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`target_user_id` INT NOT NULL,
|
||||
`sender_user_id` INT NOT NULL,
|
||||
`sender_username` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`room_id` INT NOT NULL,
|
||||
`room_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`message` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`mention_type` TINYINT NOT NULL DEFAULT 0,
|
||||
`timestamp` INT NOT NULL DEFAULT 0,
|
||||
`read` TINYINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_target_read` (`target_user_id`, `read`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`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 IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('mentions.enabled', '1'),
|
||||
('mentions.room.aliases', 'amici,friends,all,everyone,tutti,room,stanza'),
|
||||
('mentions.max.targets', '50'),
|
||||
('mentions.cooldown.ms', '3000'),
|
||||
('mentions.store.limit', '50');
|
||||
|
||||
|
||||
|
||||
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`;
|
||||
COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`;
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- 5. Per-user mention preferences (:disablementions / :disablemassmentions)
|
||||
--
|
||||
-- Read by HabboStats (default '1' = enabled), toggled by the commands.
|
||||
-- Without these columns the toggle commands cannot persist.
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
ALTER TABLE `users_settings`
|
||||
ADD COLUMN IF NOT EXISTS `mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
|
||||
COMMENT 'Receive @nick mention notifications.',
|
||||
ADD COLUMN IF NOT EXISTS `mass_mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
|
||||
COMMENT 'Receive broadcast (@all / @friends / @room) mentions.';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
-- 020_furnidata_edit_log.sql
|
||||
-- Audit trail for furnidata name/description edits made through the furni editor,
|
||||
-- plus config keys for the editor write path. NOTE: *.enabled keys elsewhere are
|
||||
-- read via Boolean.parseBoolean (true/false), but these two are numeric.
|
||||
CREATE TABLE IF NOT EXISTS `furnidata_edit_log` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`classname` varchar(255) NOT NULL,
|
||||
`action` enum('edit','revert') NOT NULL DEFAULT 'edit',
|
||||
`old_name` varchar(256) NOT NULL DEFAULT '',
|
||||
`new_name` varchar(256) NOT NULL DEFAULT '',
|
||||
`old_description` varchar(256) NOT NULL DEFAULT '',
|
||||
`new_description` varchar(256) NOT NULL DEFAULT '',
|
||||
`timestamp` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_classname` (`classname`),
|
||||
INDEX `idx_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
|
||||
('items.furnidata.edit.backup.keep','10'),
|
||||
('items.furnidata.edit.ratelimit.ms','2000'),
|
||||
-- Server-authoritative furni names (source of truth = furnidata JSON)
|
||||
('items.furnidata.names.enabled','true'),
|
||||
('items.furnidata.path',''),
|
||||
('items.furnidata.max.bytes','67108864'),
|
||||
-- Live-reload watcher
|
||||
('items.furnidata.watch.enabled','true'),
|
||||
('items.furnidata.watch.debounce.ms','750'),
|
||||
('items.furnidata.watch.min.interval.ms','5000'),
|
||||
('items.furnidata.delta.cap','500'),
|
||||
-- Furni editor: import official names/descriptions from Habbo
|
||||
('furni.editor.import.url','https://www.habbo.com/gamedata/furnidata_json/1'),
|
||||
('furni.editor.import.cache.ms','600000');
|
||||
|
||||
START TRANSACTION;
|
||||
DROP TEMPORARY TABLE IF EXISTS cleanup_furnidata_settings;
|
||||
CREATE TEMPORARY TABLE cleanup_furnidata_settings (
|
||||
`key` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL PRIMARY KEY
|
||||
);
|
||||
INSERT INTO cleanup_furnidata_settings (`key`) VALUES
|
||||
('items.furnidata.names.enabled'),
|
||||
('items.furnidata.path'),
|
||||
('items.furnidata.max.bytes'),
|
||||
('items.furnidata.watch.enabled'),
|
||||
('items.furnidata.watch.debounce.ms'),
|
||||
('items.furnidata.watch.min.interval.ms'),
|
||||
('items.furnidata.delta.cap'),
|
||||
('furni.editor.import.url'),
|
||||
('furni.editor.import.cache.ms');
|
||||
|
||||
-- Preview rows that will be removed.
|
||||
SELECT es.`key`, es.`value`
|
||||
FROM emulator_settings es
|
||||
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`
|
||||
ORDER BY es.`key`;
|
||||
|
||||
DELETE es
|
||||
FROM emulator_settings es
|
||||
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||
|
||||
-- Preview remaining matching rows inside the transaction.
|
||||
SELECT COUNT(*) AS remaining_furnidata_settings
|
||||
FROM emulator_settings es
|
||||
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||
|
||||
-- Safe default. Change to COMMIT after reviewing the preview.
|
||||
ROLLBACK;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Navigator search filters - companion to the gameserver fix for the catalog
|
||||
-- 'Find groups to join!' button (navigator/search/hotel_view/group:).
|
||||
|
||||
INSERT IGNORE INTO `navigator_filter` (`key`, `field`, `compare`, `database_query`) VALUES
|
||||
('anything', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
|
||||
('roomname', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
|
||||
('owner', 'getOwnerName', 'equals_ignore_case', 'SELECT rooms.* FROM rooms WHERE rooms.owner_name LIKE ?'),
|
||||
('tag', 'getTags', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.tags LIKE ?'),
|
||||
('group', 'getGuildName', 'contains', 'SELECT rooms.* FROM rooms INNER JOIN guilds ON guilds.room_id = rooms.id WHERE guilds.name LIKE ?');
|
||||
@@ -0,0 +1,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';
|
||||
+26
-19
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.eu.habbo</groupId>
|
||||
<artifactId>Habbo</artifactId>
|
||||
<version>4.2.32</version>
|
||||
<version>4.2.44</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -62,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>
|
||||
|
||||
@@ -77,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>
|
||||
|
||||
@@ -107,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>
|
||||
|
||||
@@ -115,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>
|
||||
|
||||
@@ -131,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>
|
||||
|
||||
@@ -139,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>
|
||||
|
||||
@@ -154,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
|
||||
@@ -172,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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
@@ -47,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;
|
||||
@@ -79,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();
|
||||
@@ -161,6 +165,10 @@ public class GameEnvironment {
|
||||
return this.itemManager;
|
||||
}
|
||||
|
||||
public FurnitureTextProvider getFurnitureTextProvider() {
|
||||
return this.furnitureTextProvider;
|
||||
}
|
||||
|
||||
public CatalogManager getCatalogManager() {
|
||||
return this.catalogManager;
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -188,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;
|
||||
|
||||
@@ -1054,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;
|
||||
}
|
||||
}
|
||||
@@ -1247,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);
|
||||
|
||||
+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,6 +191,8 @@ 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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,10 @@ public class GameClient {
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
this.dispose(true);
|
||||
}
|
||||
|
||||
public void dispose(boolean allowSessionResume) {
|
||||
try {
|
||||
this.channel.close();
|
||||
|
||||
@@ -161,7 +165,7 @@ public class GameClient {
|
||||
// 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
|
||||
@@ -177,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -123,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(" ", "")));
|
||||
}
|
||||
@@ -167,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -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 {
|
||||
@@ -149,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;
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+12
@@ -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;
|
||||
|
||||
@@ -21,6 +21,7 @@ public class HabboMention {
|
||||
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");
|
||||
@@ -33,6 +34,16 @@ public class HabboMention {
|
||||
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) {
|
||||
@@ -46,6 +57,7 @@ public class HabboMention {
|
||||
this.mentionType = mentionType;
|
||||
this.timestamp = timestamp;
|
||||
this.read = false;
|
||||
this.senderFigure = sender.getHabboInfo().getLook();
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
@@ -87,4 +99,8 @@ public class HabboMention {
|
||||
public boolean isRead() {
|
||||
return this.read;
|
||||
}
|
||||
|
||||
public String getSenderFigure() {
|
||||
return this.senderFigure == null ? "" : this.senderFigure;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,8 @@ public class MentionManager {
|
||||
|
||||
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<>();
|
||||
|
||||
// Per-user request rate limits for the incoming packets that hit the DB.
|
||||
private final ConcurrentHashMap<Integer, Long> requestListCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> markReadCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> markAllCooldowns = new ConcurrentHashMap<>();
|
||||
@@ -36,20 +33,14 @@ public class MentionManager {
|
||||
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
|
||||
}
|
||||
|
||||
/** Broadcast category resolved from a mention alias. */
|
||||
public enum BroadcastScope {
|
||||
NONE,
|
||||
// @room / @stanza - reaches the people currently in the room.
|
||||
ROOM,
|
||||
// @friends / @amici - reaches the sender's online friends, requires acc_mention_friends.
|
||||
FRIENDS,
|
||||
// @all / @everyone / @tutti - reaches every online user, requires acc_mention_everyone.
|
||||
EVERYONE
|
||||
}
|
||||
|
||||
/** Permission key (DB column) required to send an "everyone" broadcast. */
|
||||
public static final String PERMISSION_EVERYONE = "acc_mention_everyone";
|
||||
/** Permission key (DB column) required to send a "friends" broadcast. */
|
||||
public static final String PERMISSION_FRIENDS = "acc_mention_friends";
|
||||
|
||||
private Set<String> parseAliases(String configKey, String defaultValue) {
|
||||
@@ -76,14 +67,6 @@ public class MentionManager {
|
||||
return parseAliases("mentions.everyone.aliases", "all,everyone,tutti");
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify an alias candidate (lowercased, punctuation-trimmed) into a
|
||||
* broadcast scope. {@link BroadcastScope#EVERYONE} wins over
|
||||
* {@link BroadcastScope#FRIENDS} which wins over {@link BroadcastScope#ROOM}
|
||||
* so an admin who's also configured the same word into two lists gets the
|
||||
* most permissive scope (which is also the one requiring the strongest
|
||||
* permission, so it can't be misused).
|
||||
*/
|
||||
private BroadcastScope classifyAlias(String alias,
|
||||
Set<String> everyone,
|
||||
Set<String> friends,
|
||||
@@ -135,8 +118,6 @@ public class MentionManager {
|
||||
BroadcastScope scope = this.classifyAlias(aliasCandidate, everyoneAliases, friendsAliases, roomAliases);
|
||||
|
||||
if (scope != BroadcastScope.NONE) {
|
||||
// Promote to the strongest detected scope so a message with
|
||||
// both @room and @all routes through the @all permission.
|
||||
if (scope.ordinal() > broadcastScope.ordinal()) {
|
||||
broadcastScope = scope;
|
||||
}
|
||||
@@ -145,9 +126,6 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Gate the broadcast on the matching permission. If the sender does
|
||||
// not have the right to use it, drop the broadcast entirely but
|
||||
// keep processing any direct @nick tokens in the same message.
|
||||
if (broadcastScope == BroadcastScope.EVERYONE && !sender.hasPermission(PERMISSION_EVERYONE)) {
|
||||
broadcastScope = BroadcastScope.NONE;
|
||||
} else if (broadcastScope == BroadcastScope.FRIENDS && !sender.hasPermission(PERMISSION_FRIENDS)) {
|
||||
@@ -158,9 +136,6 @@ public class MentionManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stricter cooldown for broadcasts: one @all/@friends/@room expands to
|
||||
// up to mentions.max.targets DB writes and packet sends, so rate-limit it
|
||||
// separately from direct mentions.
|
||||
if (broadcastScope != BroadcastScope.NONE) {
|
||||
long roomCooldownMs = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
|
||||
Long lastRoom = this.roomBroadcastCooldowns.get(senderId);
|
||||
@@ -171,9 +146,6 @@ public class MentionManager {
|
||||
|
||||
int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50);
|
||||
if (maxTargets <= 0) maxTargets = 1;
|
||||
|
||||
// Bound the number of direct tokens we even attempt to resolve so a
|
||||
// crafted message can't push us through the room iteration N times.
|
||||
int maxDirectTokens = Math.min(directTokens.size(), maxTargets);
|
||||
|
||||
List<Habbo> targets = new ArrayList<>();
|
||||
@@ -187,7 +159,7 @@ public class MentionManager {
|
||||
this.collectFriendsTargets(sender, senderId, targets, seen, maxTargets);
|
||||
break;
|
||||
case ROOM:
|
||||
this.collectRoomTargets(room, senderId, targets, seen, maxTargets);
|
||||
this.collectRoomTargets(room, senderId, targets, seen, maxTargets, true);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
@@ -198,6 +170,9 @@ public class MentionManager {
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
|
||||
continue;
|
||||
}
|
||||
if (!acceptsMention(habbo, false)) {
|
||||
continue;
|
||||
}
|
||||
if (seen.add(habbo.getHabboInfo().getId())) {
|
||||
targets.add(habbo);
|
||||
}
|
||||
@@ -229,9 +204,10 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void collectRoomTargets(Room room, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
|
||||
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;
|
||||
}
|
||||
@@ -246,6 +222,7 @@ public class MentionManager {
|
||||
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;
|
||||
}
|
||||
@@ -254,11 +231,21 @@ public class MentionManager {
|
||||
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(
|
||||
@@ -281,9 +268,6 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Don't push a notification to the client when the INSERT did not
|
||||
// return an id - the client dedup keys on id and a 0 would skip
|
||||
// dedup entirely, opening a flood path on the next packet.
|
||||
if (generatedId <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -304,7 +288,7 @@ public class MentionManager {
|
||||
if (limit > 200) limit = 200;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM habbo_mentions WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
|
||||
"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()) {
|
||||
@@ -319,7 +303,6 @@ public class MentionManager {
|
||||
}
|
||||
|
||||
public void markRead(int userId, int mode, int mentionId) {
|
||||
// Caller has already validated mode and mentionId; this method is defensive only.
|
||||
if (mode != 0 && mode != 1) return;
|
||||
if (mode == 1 && mentionId <= 0) return;
|
||||
|
||||
@@ -351,20 +334,11 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user rate limit for {@code RequestMentionsEvent}. Returns true when
|
||||
* the caller should be served, false when it must be silently dropped.
|
||||
*/
|
||||
public boolean tryAcquireRequestList(int userId) {
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
|
||||
return tryAcquire(this.requestListCooldowns, userId, cooldownMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user rate limit for {@code MarkMentionsReadEvent}. The mark-single
|
||||
* path (mode == 1) is cheap and gets a short window; the mark-all path
|
||||
* (mode != 1) is a bulk UPDATE and gets a longer one.
|
||||
*/
|
||||
public boolean tryAcquireMarkRead(int userId, int mode) {
|
||||
long cooldownMs;
|
||||
ConcurrentHashMap<Integer, Long> bucket;
|
||||
@@ -378,9 +352,6 @@ public class MentionManager {
|
||||
return tryAcquire(bucket, userId, cooldownMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user rate limit for {@code DeleteMentionEvent}.
|
||||
*/
|
||||
public boolean tryAcquireDelete(int userId) {
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
|
||||
return tryAcquire(this.deleteCooldowns, userId, cooldownMs);
|
||||
@@ -397,11 +368,6 @@ public class MentionManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically drop cooldown entries older than the largest window so the
|
||||
* maps don't accumulate one entry per user-that-ever-played for the entire
|
||||
* server lifetime.
|
||||
*/
|
||||
private void pruneCooldownsIfDue(long now) {
|
||||
if (now - this.lastPrune < PRUNE_INTERVAL_MS) return;
|
||||
this.lastPrune = now;
|
||||
@@ -448,20 +414,49 @@ public class MentionManager {
|
||||
return value.substring(0, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a present room occupant from a raw mention token. Tries the token
|
||||
* verbatim first (so usernames containing allowed punctuation such as '-',
|
||||
* '.', '!' still match), then falls back to a trailing-punctuation-trimmed
|
||||
* form so a mention written as "@Bob!" still resolves the user "Bob".
|
||||
*/
|
||||
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)) {
|
||||
return room.getHabbo(trimmed);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -213,16 +213,16 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
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;
|
||||
@@ -239,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");
|
||||
@@ -464,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();
|
||||
@@ -484,7 +484,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
future = this.loadingFuture;
|
||||
}
|
||||
|
||||
|
||||
if (future != null) {
|
||||
try {
|
||||
future.join();
|
||||
@@ -499,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
|
||||
@@ -509,7 +509,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
shouldLoad = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait for existing load outside the lock
|
||||
if (futureToWait != null) {
|
||||
try {
|
||||
@@ -519,7 +519,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Load if needed
|
||||
if (shouldLoad) {
|
||||
this.loadDataInternal();
|
||||
@@ -559,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()) {
|
||||
@@ -654,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);
|
||||
}
|
||||
@@ -673,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;
|
||||
@@ -690,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -722,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()) {
|
||||
@@ -733,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"));
|
||||
@@ -761,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()) {
|
||||
@@ -771,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")]);
|
||||
@@ -849,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++) {
|
||||
@@ -878,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));
|
||||
@@ -1116,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);
|
||||
@@ -1183,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);
|
||||
@@ -1252,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();
|
||||
@@ -1493,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;
|
||||
}
|
||||
@@ -1600,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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1707,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() {
|
||||
@@ -1914,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());
|
||||
}
|
||||
@@ -2059,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);
|
||||
}
|
||||
|
||||
@@ -2204,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()) {
|
||||
@@ -2220,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()) {
|
||||
@@ -2338,24 +2342,24 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.wiredSettingsLoaded = true;
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
LOGGER.error("Caught SQL exception while saving wired room settings", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.pushWiredSettingsToCurrentHabbos();
|
||||
return true;
|
||||
}
|
||||
@@ -2430,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()) {
|
||||
@@ -2530,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);
|
||||
@@ -2552,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());
|
||||
@@ -2584,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) {
|
||||
@@ -2623,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);
|
||||
@@ -2632,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);
|
||||
}
|
||||
}
|
||||
@@ -2671,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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2717,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);
|
||||
}
|
||||
}
|
||||
@@ -2803,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);
|
||||
}
|
||||
|
||||
@@ -2823,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);
|
||||
}
|
||||
|
||||
@@ -2842,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);
|
||||
}
|
||||
|
||||
@@ -2869,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -300,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()) {
|
||||
@@ -322,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,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()));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -152,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); this.specialItemsById.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()); this.specialItemsById.remove(item.getId());
|
||||
synchronized (this.nests) {
|
||||
this.nests.remove(item.getId());
|
||||
}
|
||||
this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionNest> getNests() {
|
||||
@@ -174,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); this.specialItemsById.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()); this.specialItemsById.remove(item.getId());
|
||||
synchronized (this.petDrinks) {
|
||||
this.petDrinks.remove(item.getId());
|
||||
}
|
||||
this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetDrink> getPetDrinks() {
|
||||
@@ -196,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); this.specialItemsById.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()); this.specialItemsById.remove(petFood.getId());
|
||||
synchronized (this.petFoods) {
|
||||
this.petFoods.remove(petFood.getId());
|
||||
}
|
||||
this.specialItemsById.remove(petFood.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetFood> getPetFoods() {
|
||||
@@ -218,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); this.specialItemsById.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()); this.specialItemsById.remove(petToy.getId());
|
||||
synchronized (this.petToys) {
|
||||
this.petToys.remove(petToy.getId());
|
||||
}
|
||||
this.specialItemsById.remove(petToy.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetToy> getPetToys() {
|
||||
@@ -240,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); this.specialItemsById.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()); this.specialItemsById.remove(petTree.getId());
|
||||
synchronized (this.petTrees) {
|
||||
this.petTrees.remove(petTree.getId());
|
||||
}
|
||||
this.specialItemsById.remove(petTree.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetTree> getPetTrees() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+4
-1
@@ -24,8 +24,11 @@ public class PathfinderImpl implements Pathfinder {
|
||||
|
||||
private static final int CACHED_TIMEOUT_MS = Emulator.getConfig()
|
||||
.getInt(CONFIG_EXECUTION_TIME, 25);
|
||||
// Default ON: bound A* to CACHED_TIMEOUT_MS (25ms) so a pathological search
|
||||
// can't run unbounded and stall the thread. On timeout findPath returns an
|
||||
// empty path (the unit simply doesn't move there) — graceful degradation.
|
||||
private static final boolean CACHED_TIMEOUT_ENABLED = Emulator.getConfig()
|
||||
.getBoolean(CONFIG_TIMEOUT_ENABLED, false);
|
||||
.getBoolean(CONFIG_TIMEOUT_ENABLED, true);
|
||||
private static final long CACHED_TIMEOUT_NANOS = CACHED_TIMEOUT_MS * 1_000_000L;
|
||||
|
||||
private final Room room;
|
||||
|
||||
@@ -146,31 +146,23 @@ public class Habbo implements Runnable {
|
||||
this.habboInfo.setIpLogin(ip);
|
||||
}
|
||||
|
||||
if (this.client.getMachineId() == null || this.client.getMachineId().length() == 0) {
|
||||
return false;
|
||||
}
|
||||
// The Nitro client sends the UniqueID (machine fingerprint) packet right
|
||||
// AFTER the SSO ticket, so client.getMachineId() may still be null here.
|
||||
// Do NOT reject the login for a missing machineId — MachineIDEvent sets it
|
||||
// and enforces the MAC ban as soon as the UniqueID packet arrives. Only
|
||||
// MAC-ban check here when the fingerprint is already available.
|
||||
String machineId = this.client.getMachineId();
|
||||
if (machineId != null && !machineId.isEmpty()) {
|
||||
this.habboInfo.setMachineID(machineId);
|
||||
|
||||
this.habboInfo.setMachineID(this.client.getMachineId());
|
||||
|
||||
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
||||
return false;
|
||||
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Emulator.getGameEnvironment().getModToolManager().hasIPBan(this.habboInfo.getIpLogin())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.habboInfo.setMachineID(this.client.getMachineId());
|
||||
|
||||
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Emulator.getGameEnvironment().getModToolManager().hasIPBan(this.habboInfo.getIpLogin())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.habboInfo.setMachineID(this.client.getMachineId());
|
||||
this.isOnline(true);
|
||||
|
||||
this.messenger.connectionChanged(this, true, false);
|
||||
|
||||
@@ -55,6 +55,11 @@ public class HabboInfo implements Runnable {
|
||||
private RideablePet riding;
|
||||
private Class<? extends Game> currentGame;
|
||||
private TIntIntHashMap currencies;
|
||||
// Serializes credits + currencies read-modify-write and the saveCurrencies
|
||||
// snapshot so the credit-roller thread and purchase/trade handler threads
|
||||
// can't lose updates or rehash the Trove map mid-iteration. Never held
|
||||
// across run()'s DB I/O.
|
||||
private final Object currencyLock = new Object();
|
||||
private GamePlayer gamePlayer;
|
||||
private int photoRoomId;
|
||||
private int photoTimestamp;
|
||||
@@ -123,11 +128,16 @@ public class HabboInfo implements Runnable {
|
||||
}
|
||||
|
||||
private void saveCurrencies() {
|
||||
List<int[]> entries = new ArrayList<>(this.currencies.size());
|
||||
this.currencies.forEachEntry((type, amount) -> {
|
||||
entries.add(new int[]{type, amount});
|
||||
return true;
|
||||
});
|
||||
// Snapshot under the lock so a concurrent adjustOrPutValue/put can't
|
||||
// rehash the Trove map while we iterate; do the DB batch off-lock.
|
||||
List<int[]> entries;
|
||||
synchronized (this.currencyLock) {
|
||||
entries = new ArrayList<>(this.currencies.size());
|
||||
this.currencies.forEachEntry((type, amount) -> {
|
||||
entries.add(new int[]{type, amount});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
SqlQueries.batchUpdate(
|
||||
@@ -238,20 +248,30 @@ public class HabboInfo implements Runnable {
|
||||
}
|
||||
|
||||
public int getCurrencyAmount(int type) {
|
||||
return this.currencies.get(type);
|
||||
synchronized (this.currencyLock) {
|
||||
return this.currencies.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
public TIntIntHashMap getCurrencies() {
|
||||
return this.currencies;
|
||||
// Return a snapshot under the lock: callers iterate this map, which would
|
||||
// otherwise corrupt during a concurrent adjustOrPutValue rehash.
|
||||
synchronized (this.currencyLock) {
|
||||
return new TIntIntHashMap(this.currencies);
|
||||
}
|
||||
}
|
||||
|
||||
public void addCurrencyAmount(int type, int amount) {
|
||||
this.currencies.adjustOrPutValue(type, amount, amount);
|
||||
synchronized (this.currencyLock) {
|
||||
this.currencies.adjustOrPutValue(type, amount, amount);
|
||||
}
|
||||
this.run();
|
||||
}
|
||||
|
||||
public void setCurrencyAmount(int type, int amount) {
|
||||
this.currencies.put(type, amount);
|
||||
synchronized (this.currencyLock) {
|
||||
this.currencies.put(type, amount);
|
||||
}
|
||||
this.run();
|
||||
}
|
||||
|
||||
@@ -380,20 +400,26 @@ public class HabboInfo implements Runnable {
|
||||
}
|
||||
|
||||
public boolean canBuy(CatalogItem item) {
|
||||
return this.credits >= item.getCredits() && this.getCurrencies().get(item.getPointsType()) >= item.getPoints();
|
||||
return this.getCredits() >= item.getCredits() && this.getCurrencyAmount(item.getPointsType()) >= item.getPoints();
|
||||
}
|
||||
|
||||
public int getCredits() {
|
||||
return this.credits;
|
||||
synchronized (this.currencyLock) {
|
||||
return this.credits;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCredits(int credits) {
|
||||
this.credits = credits;
|
||||
synchronized (this.currencyLock) {
|
||||
this.credits = credits;
|
||||
}
|
||||
this.run();
|
||||
}
|
||||
|
||||
public void addCredits(int credits) {
|
||||
this.credits += credits;
|
||||
synchronized (this.currencyLock) {
|
||||
this.credits += credits;
|
||||
}
|
||||
this.run();
|
||||
}
|
||||
|
||||
@@ -600,6 +626,13 @@ public class HabboInfo implements Runnable {
|
||||
public void run() {
|
||||
this.saveCurrencies();
|
||||
|
||||
// Read credits under the lock so the persisted value is consistent with
|
||||
// concurrent addCredits/setCredits (matches the currencyLock invariant).
|
||||
final int creditsForSave;
|
||||
synchronized (this.currencyLock) {
|
||||
creditsForSave = this.credits;
|
||||
}
|
||||
|
||||
try {
|
||||
SqlQueries.update(
|
||||
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ?, background_border_id = ? WHERE id = ?",
|
||||
@@ -607,7 +640,7 @@ public class HabboInfo implements Runnable {
|
||||
this.online ? "1" : "0",
|
||||
this.look,
|
||||
this.gender.name(),
|
||||
this.credits,
|
||||
creditsForSave,
|
||||
Emulator.getIntUnixTimestamp(),
|
||||
this.lastOnline,
|
||||
this.homeRoom,
|
||||
|
||||
@@ -111,7 +111,7 @@ public class HabboManager {
|
||||
habbo = this.cloneCheck(userId);
|
||||
if (habbo != null) {
|
||||
habbo.alert(Emulator.getTexts().getValue("loggedin.elsewhere"));
|
||||
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
|
||||
Emulator.getGameServer().getGameClientManager().forceDisposeClient(habbo.getClient());
|
||||
habbo = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ public class HabboStats implements Runnable {
|
||||
public boolean hasGottenDefaultSavedSearches;
|
||||
private HabboInfo habboInfo;
|
||||
private boolean allowTrade;
|
||||
private boolean mentionsEnabled;
|
||||
private boolean massMentionsEnabled;
|
||||
private int clubExpireTimestamp;
|
||||
private int muteEndTime;
|
||||
public int maxFriends;
|
||||
@@ -131,6 +133,8 @@ public class HabboStats implements Runnable {
|
||||
this.guilds = new ArrayList<>();
|
||||
this.tags = set.getString("tags").split(";");
|
||||
this.allowTrade = set.getString("can_trade").equals("1");
|
||||
this.mentionsEnabled = "1".equals(safeColumnString(set, "mentions_enabled", "1"));
|
||||
this.massMentionsEnabled = "1".equals(safeColumnString(set, "mass_mentions_enabled", "1"));
|
||||
this.votedRooms = new TIntArrayStack();
|
||||
this.clubExpireTimestamp = set.getInt("club_expire_timestamp");
|
||||
this.loginStreak = set.getInt("login_streak");
|
||||
@@ -444,14 +448,26 @@ public class HabboStats implements Runnable {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.achievementProgress.containsKey(achievement))
|
||||
return this.achievementProgress.get(achievement);
|
||||
|
||||
return -1;
|
||||
synchronized (this.achievementProgress) {
|
||||
Integer progress = this.achievementProgress.get(achievement);
|
||||
return progress != null ? progress : -1;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgress(Achievement achievement, int progress) {
|
||||
this.achievementProgress.put(achievement, progress);
|
||||
synchronized (this.achievementProgress) {
|
||||
this.achievementProgress.put(achievement, progress);
|
||||
}
|
||||
}
|
||||
|
||||
/** Atomic read-add-write so concurrent progress sources don't lose updates. Returns the new total. */
|
||||
public int incrementProgress(Achievement achievement, int amount) {
|
||||
synchronized (this.achievementProgress) {
|
||||
Integer current = this.achievementProgress.get(achievement);
|
||||
int next = (current != null ? current : 0) + amount;
|
||||
this.achievementProgress.put(achievement, next);
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
public int getRentedTimeEnd() {
|
||||
@@ -749,13 +765,6 @@ public class HabboStats implements Runnable {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore an user.
|
||||
*
|
||||
* @param gameClient The client to which this HabboStats instance belongs.
|
||||
* @param userId The user to ignore.
|
||||
* @return true if successfully ignored, false otherwise.
|
||||
*/
|
||||
public boolean ignoreUser(GameClient gameClient, int userId) {
|
||||
final Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
@@ -805,6 +814,44 @@ public class HabboStats implements Runnable {
|
||||
else return this.allowTrade;
|
||||
}
|
||||
|
||||
public boolean mentionsEnabled() {
|
||||
return this.mentionsEnabled;
|
||||
}
|
||||
|
||||
public boolean massMentionsEnabled() {
|
||||
return this.massMentionsEnabled;
|
||||
}
|
||||
|
||||
public void setMentionsEnabled(boolean enabled) {
|
||||
this.mentionsEnabled = enabled;
|
||||
persistFlag("mentions_enabled", enabled);
|
||||
}
|
||||
|
||||
public void setMassMentionsEnabled(boolean enabled) {
|
||||
this.massMentionsEnabled = enabled;
|
||||
persistFlag("mass_mentions_enabled", enabled);
|
||||
}
|
||||
|
||||
private void persistFlag(String column, boolean enabled) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET `" + column + "` = ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setString(1, enabled ? "1" : "0");
|
||||
statement.setInt(2, this.habboInfo.getId());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to persist users_settings.{} for user {}", column, this.habboInfo.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeColumnString(ResultSet set, String column, String defaultValue) {
|
||||
try {
|
||||
String value = set.getString(column);
|
||||
return value == null ? defaultValue : value;
|
||||
} catch (SQLException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public void setAllowTrade(boolean allowTrade) {
|
||||
this.allowTrade = allowTrade;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,15 @@ public class WiredHandler {
|
||||
private static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final LegacyExecutionPlan executionPlan) {
|
||||
long millis = System.currentTimeMillis();
|
||||
int roomUnitId = roomUnit != null ? roomUnit.getId() : -1;
|
||||
|
||||
// Only one thread may process a given trigger box at a time, so the
|
||||
// cooldown check (below) and setCooldown (further down) act as one
|
||||
// atomic claim — preventing a concurrent packet/cycle double-fire.
|
||||
if (!trigger.tryBeginProcessing()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Emulator.isReady && ((Emulator.getConfig().getBoolean("wired.custom.enabled", false) && (trigger.canExecute(millis) || roomUnitId > -1) && trigger.userCanExecute(roomUnitId, millis)) || (!Emulator.getConfig().getBoolean("wired.custom.enabled", false) && trigger.canExecute(millis))) && trigger.execute(roomUnit, room, stuff)) {
|
||||
THashSet<InteractionWiredCondition> conditions = room.getRoomSpecialTypes().getConditions(trigger.getX(), trigger.getY());
|
||||
THashSet<InteractionWiredEffect> effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY());
|
||||
@@ -272,6 +281,9 @@ public class WiredHandler {
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
trigger.endProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean evaluateConditions(THashSet<InteractionWiredCondition> conditions, RoomUnit roomUnit, Room room, Object[] stuff, int evaluationMode, int evaluationValue) {
|
||||
|
||||
+1
-1
@@ -279,7 +279,7 @@ public final class WiredTextPlaceholderUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
String furniName = item.getBaseItem().getFullName();
|
||||
String furniName = item.getBaseItem().getDisplayName();
|
||||
if (furniName == null || furniName.trim().isEmpty()) {
|
||||
furniName = item.getBaseItem().getName();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.eu.habbo.util.PacketUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ClientMessage {
|
||||
private final int header;
|
||||
private final ByteBuf buffer;
|
||||
@@ -61,10 +63,17 @@ public class ClientMessage {
|
||||
|
||||
public String readString() {
|
||||
try {
|
||||
int length = this.readShort();
|
||||
// Length is an unsigned short in the protocol; mask to avoid a
|
||||
// negative array size, and clamp to what's actually buffered so a
|
||||
// bogus length can't throw mid-read and desync the remaining fields.
|
||||
int length = this.readShort() & 0xFFFF;
|
||||
int available = this.buffer.readableBytes();
|
||||
if (length > available) {
|
||||
length = available;
|
||||
}
|
||||
byte[] data = new byte[length];
|
||||
this.buffer.readBytes(data);
|
||||
return new String(data);
|
||||
return new String(data, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -285,6 +285,9 @@ public class PacketManager {
|
||||
this.registerHandler(Incoming.FurniEditorInteractionsEvent, FurniEditorInteractionsEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorUpdateEvent, FurniEditorUpdateEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorDeleteEvent, FurniEditorDeleteEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorUpdateFurnidataEvent, FurniEditorUpdateFurnidataEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorRevertFurnidataEvent, FurniEditorRevertFurnidataEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorImportTextEvent, FurniEditorImportTextEvent.class);
|
||||
|
||||
// Catalog Admin
|
||||
this.registerHandler(Incoming.CatalogAdminSavePageEvent, CatalogAdminSavePageEvent.class);
|
||||
@@ -298,6 +301,8 @@ public class PacketManager {
|
||||
this.registerHandler(Incoming.CatalogAdminPublishEvent, CatalogAdminPublishEvent.class);
|
||||
this.registerHandler(Incoming.CatalogAdminSavePageImagesEvent, CatalogAdminSavePageImagesEvent.class);
|
||||
this.registerHandler(Incoming.CatalogAdminSavePageIconEvent, CatalogAdminSavePageIconEvent.class);
|
||||
this.registerHandler(Incoming.CatalogAdminLoadOfferEvent, CatalogAdminLoadOfferEvent.class);
|
||||
this.registerHandler(Incoming.CatalogAdminLoadPageEvent, CatalogAdminLoadPageEvent.class);
|
||||
}
|
||||
|
||||
private void registerEvent() throws Exception {
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.buffer.Unpooled;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ServerMessage {
|
||||
|
||||
@@ -61,7 +62,7 @@ public class ServerMessage {
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] data = obj.getBytes();
|
||||
byte[] data = obj.getBytes(StandardCharsets.UTF_8);
|
||||
this.stream.writeShort(data.length);
|
||||
this.stream.write(data);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -431,6 +431,9 @@ public class Incoming {
|
||||
public static final int FurniEditorInteractionsEvent = 10043;
|
||||
public static final int FurniEditorUpdateEvent = 10044;
|
||||
public static final int FurniEditorDeleteEvent = 10045;
|
||||
public static final int FurniEditorUpdateFurnidataEvent = 10046;
|
||||
public static final int FurniEditorRevertFurnidataEvent = 10048;
|
||||
public static final int FurniEditorImportTextEvent = 10049;
|
||||
|
||||
// Catalog Admin
|
||||
public static final int CatalogAdminSavePageEvent = 10050;
|
||||
@@ -444,6 +447,8 @@ public class Incoming {
|
||||
public static final int CatalogAdminPublishEvent = 10058;
|
||||
public static final int CatalogAdminSavePageImagesEvent = 10060;
|
||||
public static final int CatalogAdminSavePageIconEvent = 10061;
|
||||
public static final int CatalogAdminLoadOfferEvent = 10062;
|
||||
public static final int CatalogAdminLoadPageEvent = 10063;
|
||||
|
||||
// Custom Prefixes
|
||||
public static final int RequestUserPrefixesEvent = 7011;
|
||||
|
||||
+2
-2
@@ -248,7 +248,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
LOGGER.debug("sender reached daily total LTD limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getDisplayName())
|
||||
.replace("%limit%", ltdLimit + "")
|
||||
);
|
||||
return;
|
||||
@@ -259,7 +259,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
LOGGER.debug("sender reached daily LTD item limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getDisplayName())
|
||||
.replace("%limit%", ltdLimit + "")
|
||||
);
|
||||
return;
|
||||
|
||||
+4
-18
@@ -14,11 +14,7 @@ import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||
import com.eu.habbo.habbohotel.users.HabboInventory;
|
||||
import com.eu.habbo.habbohotel.users.subscriptions.Subscription;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseFailedComposer;
|
||||
import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseUnavailableComposer;
|
||||
import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer;
|
||||
import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer;
|
||||
import com.eu.habbo.messages.outgoing.catalog.PurchaseOKComposer;
|
||||
import com.eu.habbo.messages.outgoing.catalog.*;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer;
|
||||
@@ -52,6 +48,8 @@ public class CatalogBuyItemEvent extends MessageHandler {
|
||||
int itemId = this.packet.readInt();
|
||||
String extraData = this.packet.readString();
|
||||
int count = this.packet.readInt();
|
||||
if (count < 1) count = 1;
|
||||
if (count > 100) count = 100;
|
||||
|
||||
try {
|
||||
if (this.client.getHabbo().getInventory().getItemsComponent().itemCount() > HabboInventory.MAXIMUM_ITEMS) {
|
||||
@@ -203,12 +201,6 @@ public class CatalogBuyItemEvent extends MessageHandler {
|
||||
else
|
||||
item = page.getCatalogItem(itemId);
|
||||
|
||||
// Search-results buy sends the catalog offer_id as itemId
|
||||
// (FurnitureOffer.offerId is derived from furnidata's
|
||||
// purchaseOfferId, which matches `catalog_items.offer_id`),
|
||||
// not the `catalog_items.id` primary key that getCatalogItem
|
||||
// expects. Fall back to scanning the page for the matching
|
||||
// offer_id so the search → buy flow works.
|
||||
if (item == null && !(page instanceof RecentPurchasesLayout)) {
|
||||
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
|
||||
if (candidate != null && candidate.getOfferId() == itemId) {
|
||||
@@ -217,13 +209,7 @@ public class CatalogBuyItemEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Inventory cap check based on the actual base items the
|
||||
// purchase will create, not the page layout - bots/pets
|
||||
// can legitimately live on bundle pages, search results,
|
||||
// recent-purchases, etc., and the layout-instanceof check
|
||||
// missed all those paths. Mirrors the bot/pet branches
|
||||
// inside CatalogManager.purchaseItem (Item.isBot / isPet
|
||||
// and the same prefix check) so detection stays in sync.
|
||||
|
||||
boolean itemHasBot = false;
|
||||
boolean itemHasPet = false;
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminOfferDetailsComposer;
|
||||
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
public class CatalogAdminLoadOfferEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
int offerId = this.packet.readInt();
|
||||
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
|
||||
|
||||
String sql = (pageType == CatalogPageType.BUILDER)
|
||||
? "SELECT id, order_number FROM catalog_items_bc WHERE id = ? LIMIT 1"
|
||||
: "SELECT id, offer_id, limited_stack, order_number FROM catalog_items WHERE id = ? LIMIT 1";
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setInt(1, offerId);
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (!set.next()) return;
|
||||
|
||||
if (pageType == CatalogPageType.BUILDER) {
|
||||
this.client.sendResponse(new CatalogAdminOfferDetailsComposer(
|
||||
set.getInt("id"),
|
||||
0,
|
||||
0,
|
||||
set.getInt("order_number")
|
||||
));
|
||||
} else {
|
||||
this.client.sendResponse(new CatalogAdminOfferDetailsComposer(
|
||||
set.getInt("id"),
|
||||
set.getInt("offer_id"),
|
||||
set.getInt("limited_stack"),
|
||||
set.getInt("order_number")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogPage;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminPageDetailsComposer;
|
||||
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
|
||||
|
||||
public class CatalogAdminLoadPageEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
int pageId = this.packet.readInt();
|
||||
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
|
||||
|
||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
|
||||
if (page == null) return;
|
||||
|
||||
this.client.sendResponse(new CatalogAdminPageDetailsComposer(page));
|
||||
}
|
||||
}
|
||||
+5
@@ -29,6 +29,11 @@ public class OpenRecycleBoxEvent extends MessageHandler {
|
||||
if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId()) return;
|
||||
|
||||
if (item instanceof InteractionGift) {
|
||||
// The actual unwrap (OpenGift) runs async/delayed and only then
|
||||
// removes the wrapper, so a second packet would otherwise pass
|
||||
// the room/owner checks and double-process the gift. Claim it once.
|
||||
if (!((InteractionGift) item).tryStartOpening()) return;
|
||||
|
||||
if (item.getBaseItem().getName().contains("present_wrap")) {
|
||||
((InteractionGift) item).explode = true;
|
||||
room.updateItem(item);
|
||||
|
||||
+10
-7
@@ -40,23 +40,26 @@ public class RecycleEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (items.size() == count) {
|
||||
for (HabboItem item : items) {
|
||||
this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
|
||||
this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
|
||||
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
|
||||
}
|
||||
} else {
|
||||
if (items.size() != count) {
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute the reward BEFORE consuming the inputs. Previously the
|
||||
// inputs were deleted first, so a null reward (misconfiguration)
|
||||
// permanently destroyed the 8 furni with nothing in return.
|
||||
HabboItem reward = Emulator.getGameEnvironment().getItemManager().handleRecycle(this.client.getHabbo(), Emulator.getGameEnvironment().getCatalogManager().getRandomRecyclerPrize().getId() + "");
|
||||
if (reward == null) {
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
for (HabboItem item : items) {
|
||||
this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
|
||||
this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
|
||||
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
|
||||
}
|
||||
|
||||
this.client.sendResponse(new AddHabboItemComposer(reward));
|
||||
this.client.getHabbo().getInventory().getItemsComponent().addItem(reward);
|
||||
this.client.sendResponse(new RecyclerCompleteComposer(RecyclerCompleteComposer.RECYCLING_COMPLETE));
|
||||
|
||||
+17
@@ -37,6 +37,8 @@ public class CraftingCraftItemEvent extends MessageHandler {
|
||||
HabboItem habboItem = this.client.getHabbo().getInventory().getItemsComponent().getAndRemoveHabboItem(set.getKey());
|
||||
|
||||
if (habboItem == null) {
|
||||
// Not enough ingredients — give back whatever we already pulled.
|
||||
this.restoreItems(toRemove);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,8 +72,23 @@ public class CraftingCraftItemEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reward creation failed after we already pulled the ingredients —
|
||||
// restore them so the craft isn't a silent item sink.
|
||||
this.restoreItems(toRemove);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new CraftingResultComposer(null));
|
||||
}
|
||||
|
||||
private void restoreItems(TIntObjectHashMap<HabboItem> items) {
|
||||
if (items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
items.forEachValue(item -> {
|
||||
this.client.getHabbo().getInventory().getItemsComponent().addItem(item);
|
||||
this.client.sendResponse(new AddHabboItemComposer(item));
|
||||
return true;
|
||||
});
|
||||
this.client.sendResponse(new InventoryRefreshComposer());
|
||||
}
|
||||
}
|
||||
|
||||
+338
-13
@@ -1,6 +1,7 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataSourceResolver;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -9,12 +10,15 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Manages reading and writing of FurnitureData entries.
|
||||
@@ -43,24 +47,184 @@ public class FurniDataManager {
|
||||
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 static volatile CachedIndex cachedIndex = null;
|
||||
|
||||
public record LookupResult(String itemJson, String diagnosticJson) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON string for a specific item.
|
||||
* Returns "{}" if not found or on error.
|
||||
*/
|
||||
public static String getItemJson(int itemId) {
|
||||
try {
|
||||
ResolvedSource source = resolveSource();
|
||||
if (source == null) return "{}";
|
||||
return getItemJson(itemId, null);
|
||||
}
|
||||
|
||||
if (source.directory) {
|
||||
return findItemInSplitDir(source.path, itemId);
|
||||
/**
|
||||
* Get the JSON string for a specific item.
|
||||
* Prefer the DB classname because items_base.id can diverge from the
|
||||
* furnidata id after imports/reconciliations. Falls back to id lookup.
|
||||
* Returns "{}" if not found or on error.
|
||||
*/
|
||||
public static String getItemJson(int itemId, String classname) {
|
||||
return getItemLookup(itemId, classname).itemJson();
|
||||
}
|
||||
|
||||
public static LookupResult getItemLookup(int itemId, String classname) {
|
||||
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve();
|
||||
if (source == null || !source.ok()) {
|
||||
return new LookupResult("{}", diagnostic(source, itemId, classname, "source_missing"));
|
||||
}
|
||||
|
||||
try {
|
||||
CachedIndex index = indexFor(source);
|
||||
|
||||
// 1. Try exact classname match (preserves *N suffix for multicolor items)
|
||||
String key = baseClassname(classname);
|
||||
String byClassname = key != null ? index.byClassname.get(key) : null;
|
||||
if (byClassname != null) {
|
||||
return new LookupResult(byClassname, diagnostic(source, itemId, classname, "matched_classname"));
|
||||
}
|
||||
|
||||
if (!Files.exists(source.path)) return "{}";
|
||||
// 2. Fallback: try stripped classname (without *N suffix) for items whose
|
||||
// furnidata entry does not include the color-variant suffix.
|
||||
String strippedKey = strippedClassname(classname);
|
||||
if (strippedKey != null && !strippedKey.equals(key)) {
|
||||
String byStripped = index.byClassname.get(strippedKey);
|
||||
if (byStripped != null) {
|
||||
return new LookupResult(byStripped, diagnostic(source, itemId, classname, "matched_classname_stripped"));
|
||||
}
|
||||
}
|
||||
|
||||
String content = readJson5(source.path);
|
||||
return findItemInRoot(JsonParser.parseString(content).getAsJsonObject(), itemId);
|
||||
String byId = index.byId.get(itemId);
|
||||
if (byId != null) {
|
||||
return new LookupResult(byId, diagnostic(source, itemId, classname, "matched_id"));
|
||||
}
|
||||
|
||||
String reason = index.empty ? "manifest_empty" : "not_found";
|
||||
return new LookupResult("{}", diagnostic(source, itemId, classname, reason));
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to read FurnitureData for item " + itemId, e);
|
||||
FurnidataSourceResolver.Source errorSource = new FurnidataSourceResolver.Source(source.path(), source.directory(), FurnidataSourceResolver.Status.ERROR, e.getMessage());
|
||||
return new LookupResult("{}", diagnostic(errorSource, itemId, classname, "error"));
|
||||
}
|
||||
}
|
||||
|
||||
private static CachedIndex indexFor(FurnidataSourceResolver.Source source) {
|
||||
long signature = sourceSignature(source.path());
|
||||
String sourceKey = source.path().toAbsolutePath().normalize().toString();
|
||||
CachedIndex current = cachedIndex;
|
||||
if (current != null && current.sourceKey.equals(sourceKey) && current.signature == signature) return current;
|
||||
|
||||
CachedIndex next = buildIndex(source, sourceKey, signature);
|
||||
cachedIndex = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
private static CachedIndex buildIndex(FurnidataSourceResolver.Source source, String sourceKey, long signature) {
|
||||
Map<Integer, String> byId = new HashMap<>();
|
||||
Map<String, String> byClassname = new HashMap<>();
|
||||
|
||||
if (source.directory()) {
|
||||
indexSplitDir(source.path(), byId, byClassname);
|
||||
} else {
|
||||
try {
|
||||
String content = readJson5(source.path());
|
||||
indexRoot(JsonParser.parseString(content).getAsJsonObject(), byId, byClassname);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to parse furnidata source {}", source.path(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return new CachedIndex(sourceKey, signature, Map.copyOf(byId), Map.copyOf(byClassname), byId.isEmpty() && byClassname.isEmpty());
|
||||
}
|
||||
|
||||
private static void indexSplitDir(Path baseDir, Map<Integer, String> byId, Map<String, String> byClassname) {
|
||||
if (!Files.isDirectory(baseDir)) return;
|
||||
|
||||
for (String tier : readTiersManifest(baseDir)) {
|
||||
Path tierDir = baseDir.resolve(tier);
|
||||
if (!Files.isDirectory(tierDir)) continue;
|
||||
|
||||
for (String fileName : readFilesManifest(tierDir)) {
|
||||
Path file = tierDir.resolve(fileName);
|
||||
if (!Files.exists(file)) continue;
|
||||
|
||||
try {
|
||||
String content = readJson5(file);
|
||||
indexRoot(JsonParser.parseString(content).getAsJsonObject(), byId, byClassname);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to parse split gamedata file " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void indexRoot(JsonObject root, Map<Integer, String> byId, Map<String, String> byClassname) {
|
||||
for (String section : SECTIONS) {
|
||||
if (!root.has(section)) continue;
|
||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||
if (!sectionObj.has("furnitype")) continue;
|
||||
|
||||
for (JsonElement el : sectionObj.getAsJsonArray("furnitype")) {
|
||||
JsonObject obj = el.getAsJsonObject();
|
||||
String json = obj.toString();
|
||||
|
||||
if (obj.has("id")) byId.put(obj.get("id").getAsInt(), json);
|
||||
if (obj.has("classname")) {
|
||||
String key = baseClassname(obj.get("classname").getAsString());
|
||||
if (key != null) byClassname.put(key, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long sourceSignature(Path source) {
|
||||
try {
|
||||
if (source == null || !Files.exists(source)) return -1L;
|
||||
if (!Files.isDirectory(source)) return Files.getLastModifiedTime(source).toMillis() ^ Files.size(source);
|
||||
|
||||
final long[] signature = { 17L };
|
||||
try (var stream = Files.walk(source)) {
|
||||
stream.filter(Files::isRegularFile).forEach(path -> {
|
||||
try {
|
||||
signature[0] = (signature[0] * 31L) ^ Files.getLastModifiedTime(path).toMillis() ^ Files.size(path);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
return signature[0];
|
||||
} catch (Exception e) {
|
||||
return System.nanoTime();
|
||||
}
|
||||
}
|
||||
|
||||
private static String diagnostic(FurnidataSourceResolver.Source source, int itemId, String classname, String reason) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("reason", reason);
|
||||
obj.addProperty("itemId", itemId);
|
||||
obj.addProperty("classname", classname != null ? classname : "");
|
||||
obj.addProperty("sourcePath", source != null && source.path() != null ? source.path().toString() : "");
|
||||
obj.addProperty("sourceDirectory", source != null && source.directory());
|
||||
obj.addProperty("sourceStatus", source != null ? source.status().name() : "CONFIG_MISSING");
|
||||
obj.addProperty("message", source != null && source.message() != null ? source.message() : "");
|
||||
return obj.toString();
|
||||
}
|
||||
|
||||
private record CachedIndex(String sourceKey, long signature, Map<Integer, String> byId, Map<String, String> byClassname, boolean empty) {
|
||||
}
|
||||
|
||||
static String findItemJson(Path source, boolean directory, int itemId, String classname) {
|
||||
try {
|
||||
if (directory) {
|
||||
return findItemInSplitDir(source, itemId, classname);
|
||||
}
|
||||
|
||||
if (!Files.exists(source)) return "{}";
|
||||
|
||||
String content = readJson5(source);
|
||||
String found = findItemInRoot(JsonParser.parseString(content).getAsJsonObject(), itemId, classname);
|
||||
return found != null ? found : "{}";
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to read FurnitureData for item " + itemId, e);
|
||||
}
|
||||
@@ -69,6 +233,13 @@ public class FurniDataManager {
|
||||
}
|
||||
|
||||
private static String findItemInRoot(JsonObject root, int itemId) {
|
||||
return findItemInRoot(root, itemId, null);
|
||||
}
|
||||
|
||||
private static String findItemInRoot(JsonObject root, int itemId, String classname) {
|
||||
String byClassname = findItemInRootByClassname(root, classname);
|
||||
if (byClassname != null) return byClassname;
|
||||
|
||||
for (String section : SECTIONS) {
|
||||
if (!root.has(section)) continue;
|
||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||
@@ -85,11 +256,80 @@ public class FurniDataManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String findItemInRootByClassname(JsonObject root, String classname) {
|
||||
String wanted = baseClassname(classname);
|
||||
if (wanted == null) return null;
|
||||
|
||||
for (String section : SECTIONS) {
|
||||
if (!root.has(section)) continue;
|
||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||
if (!sectionObj.has("furnitype")) continue;
|
||||
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||
|
||||
for (JsonElement el : types) {
|
||||
JsonObject obj = el.getAsJsonObject();
|
||||
if (!obj.has("classname")) continue;
|
||||
|
||||
// Try exact match first (preserves *N suffix)
|
||||
String actual = baseClassname(obj.get("classname").getAsString());
|
||||
if (wanted.equals(actual)) return obj.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try stripped classname (without *N suffix)
|
||||
String strippedWanted = strippedClassname(classname);
|
||||
if (strippedWanted != null && !strippedWanted.equals(wanted)) {
|
||||
for (String section : SECTIONS) {
|
||||
if (!root.has(section)) continue;
|
||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||
if (!sectionObj.has("furnitype")) continue;
|
||||
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||
|
||||
for (JsonElement el : types) {
|
||||
JsonObject obj = el.getAsJsonObject();
|
||||
if (!obj.has("classname")) continue;
|
||||
|
||||
String actual = strippedClassname(obj.get("classname").getAsString());
|
||||
if (strippedWanted.equals(actual)) return obj.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a classname for index/lookup.
|
||||
*
|
||||
* Preserves the full classname including any {@code *N} color-variant suffix
|
||||
* so that multicolor items (e.g. {@code rare_dragonlamp*1}, {@code rare_dragonlamp*2})
|
||||
* each get their own index entry. The stripped variant (without {@code *N}) is
|
||||
* used as a fallback during lookup for items whose furnidata entry does not
|
||||
* include the suffix.
|
||||
*/
|
||||
private static String baseClassname(String classname) {
|
||||
if (classname == null) return null;
|
||||
String base = classname.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
return base.isEmpty() ? null : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #baseClassname(String)} but strips any trailing {@code *N}
|
||||
* color-variant suffix. Used as a fallback during lookup.
|
||||
*/
|
||||
private static String strippedClassname(String classname) {
|
||||
if (classname == null) return null;
|
||||
int star = classname.indexOf('*');
|
||||
String base = star >= 0 ? classname.substring(0, star) : classname;
|
||||
base = base.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
return base.isEmpty() ? null : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the split directory layout looking for an item by id.
|
||||
* Later tiers (custom, then seasonal) override earlier ones.
|
||||
*/
|
||||
private static String findItemInSplitDir(Path baseDir, int itemId) {
|
||||
private static String findItemInSplitDir(Path baseDir, int itemId, String classname) {
|
||||
if (!Files.isDirectory(baseDir)) return "{}";
|
||||
|
||||
List<String> tiers = readTiersManifest(baseDir);
|
||||
@@ -107,7 +347,7 @@ public class FurniDataManager {
|
||||
try {
|
||||
String content = readJson5(file);
|
||||
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
|
||||
String match = findItemInRoot(obj, itemId);
|
||||
String match = findItemInRoot(obj, itemId, classname);
|
||||
if (match != null) found = match;
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to parse split gamedata file " + file, e);
|
||||
@@ -239,7 +479,7 @@ public class FurniDataManager {
|
||||
* Represents the resolved location of the furnidata source: either a single
|
||||
* file or a directory in split-layout mode.
|
||||
*/
|
||||
private static class ResolvedSource {
|
||||
static class ResolvedSource {
|
||||
final Path path;
|
||||
final boolean directory;
|
||||
|
||||
@@ -270,9 +510,9 @@ public class FurniDataManager {
|
||||
|
||||
if (!rendererObj.has("furnidata.url")) return null;
|
||||
|
||||
String furniUrl = rendererObj.get("furnidata.url").getAsString();
|
||||
String furniUrl = expandRendererUrl(rendererObj, "furnidata.url");
|
||||
|
||||
if (furniUrl.contains("${")) {
|
||||
if (hasUnresolvedPathPlaceholder(furniUrl)) {
|
||||
Path fallback = fallbackToBasePath();
|
||||
return fallback != null ? new ResolvedSource(fallback, Files.isDirectory(fallback)) : null;
|
||||
}
|
||||
@@ -296,6 +536,9 @@ public class FurniDataManager {
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (basePath.isEmpty()) return null;
|
||||
|
||||
ResolvedSource mapped = toLocalSource(Paths.get(basePath), furniUrl);
|
||||
if (mapped != null) return mapped;
|
||||
|
||||
if (splitMode) {
|
||||
// Derive the directory name from the URL: take the last non-empty
|
||||
// segment before the trailing slash. e.g. https://x/y/furnidata/ -> "furnidata"
|
||||
@@ -326,4 +569,86 @@ public class FurniDataManager {
|
||||
if (Files.exists(legacy)) return legacy;
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
String replacement = rendererObj.get(placeholder).getAsString();
|
||||
value = value.substring(0, start) + replacement + value.substring(end + 1);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean hasUnresolvedPathPlaceholder(String value) {
|
||||
if (value == null) return false;
|
||||
|
||||
String pathOnly = stripQueryAndFragment(value);
|
||||
return pathOnly.contains("${");
|
||||
}
|
||||
|
||||
static ResolvedSource 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 ResolvedSource(local, splitMode || Files.isDirectory(local));
|
||||
}
|
||||
|
||||
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 normalizedUrlPath = urlPath.replace('\\', '/');
|
||||
String baseName = assetBase.getFileName() != null ? assetBase.getFileName().toString() : "";
|
||||
String marker = "/" + baseName + "/";
|
||||
|
||||
Path candidate;
|
||||
int markerIndex = baseName.isEmpty() ? -1 : normalizedUrlPath.indexOf(marker);
|
||||
if (markerIndex >= 0) {
|
||||
String relative = normalizedUrlPath.substring(markerIndex + marker.length());
|
||||
candidate = assetBase.resolve(relative);
|
||||
} else if (splitMode) {
|
||||
String trimmed = normalizedUrlPath.endsWith("/")
|
||||
? normalizedUrlPath.substring(0, normalizedUrlPath.length() - 1)
|
||||
: normalizedUrlPath;
|
||||
String dirName = trimmed.substring(trimmed.lastIndexOf('/') + 1);
|
||||
candidate = assetBase.resolve(dirName);
|
||||
} else {
|
||||
String filename = normalizedUrlPath.substring(normalizedUrlPath.lastIndexOf('/') + 1);
|
||||
candidate = assetBase.resolve(filename);
|
||||
}
|
||||
|
||||
return new ResolvedSource(candidate, splitMode || Files.isDirectory(candidate));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -41,6 +41,7 @@ public class FurniEditorDetailEvent extends MessageHandler {
|
||||
int usageCount = 0;
|
||||
List<Map<String, Object>> catalogItems = new ArrayList<>();
|
||||
String furniDataJson = "{}";
|
||||
String furniDataDiagnosticJson = "{}";
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
// Load full item data
|
||||
@@ -86,11 +87,15 @@ public class FurniEditorDetailEvent extends MessageHandler {
|
||||
|
||||
// Try to read furnidata.json entry
|
||||
try {
|
||||
furniDataJson = FurniDataManager.getItemJson(itemId);
|
||||
Object classname = item.get("item_name");
|
||||
FurniDataManager.LookupResult lookup = FurniDataManager.getItemLookup(itemId, classname != null ? classname.toString() : null);
|
||||
furniDataJson = lookup.itemJson();
|
||||
furniDataDiagnosticJson = lookup.diagnosticJson();
|
||||
} catch (Exception e) {
|
||||
furniDataJson = "{}";
|
||||
furniDataDiagnosticJson = "{}";
|
||||
}
|
||||
|
||||
client.sendResponse(new FurniEditorDetailComposer(item, usageCount, catalogItems, furniDataJson));
|
||||
client.sendResponse(new FurniEditorDetailComposer(item, usageCount, catalogItems, furniDataJson, furniDataDiagnosticJson));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ public class FurniEditorHelper {
|
||||
* Prevents SQL injection via arbitrary column names.
|
||||
*/
|
||||
public static final java.util.Set<String> ALLOWED_UPDATE_FIELDS = java.util.Set.of(
|
||||
"item_name", "public_name", "sprite_id", "type", "width", "length",
|
||||
"public_name", "sprite_id", "type", "width", "length",
|
||||
"stack_height", "allow_stack", "allow_walk", "allow_sit", "allow_lay",
|
||||
"allow_gift", "allow_trade", "allow_recycle", "allow_marketplace_sell",
|
||||
"allow_inventory_stack", "interaction_type", "interaction_modes_count",
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorImportTextResultComposer;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
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.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Incoming 10049 — admin imports the official Habbo display name/description for a
|
||||
* furni's classname from a configured furnidata URL (e.g.
|
||||
* https://www.habbo.it/gamedata/furnidata_json/1). The fetched text only POPULATES
|
||||
* the editor fields client-side; the admin reviews and Saves via the normal flow.
|
||||
*
|
||||
* Source URL is admin-configured in emulator_settings ({@code furni.editor.import.url}),
|
||||
* never supplied by the client (no SSRF). The remote furnidata is cached with a TTL.
|
||||
*/
|
||||
public class FurniEditorImportTextEvent extends MessageHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FurniEditorImportTextEvent.class);
|
||||
private static final List<String> SECTIONS = Arrays.asList("roomitemtypes", "wallitemtypes");
|
||||
|
||||
// Shared TTL cache (the remote furnidata is multi-MB — do not refetch per click).
|
||||
private static volatile JsonObject CACHE;
|
||||
private static volatile String CACHE_URL;
|
||||
private static volatile long CACHE_TIME;
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
int itemId = this.packet.readInt();
|
||||
if (itemId <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
String classname = FurniEditorUpdateFurnidataEvent.classnameForItem(itemId);
|
||||
if (classname == null) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found"));
|
||||
return;
|
||||
}
|
||||
String cn = classname.trim().toLowerCase(Locale.ROOT);
|
||||
|
||||
String url = Emulator.getConfig().getValue(
|
||||
"furni.editor.import.url", "https://www.habbo.it/gamedata/furnidata_json/1");
|
||||
if (url == null || !(url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Import source not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject root = fetchCached(url);
|
||||
if (root == null) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Could not fetch Habbo furnidata"));
|
||||
return;
|
||||
}
|
||||
|
||||
String foundName = null, foundDesc = null;
|
||||
outer:
|
||||
for (String section : SECTIONS) {
|
||||
if (!root.has(section) || !root.get(section).isJsonObject()) continue;
|
||||
JsonObject sec = root.getAsJsonObject(section);
|
||||
if (!sec.has("furnitype") || !sec.get("furnitype").isJsonArray()) continue;
|
||||
for (JsonElement el : sec.getAsJsonArray("furnitype")) {
|
||||
if (!el.isJsonObject()) continue;
|
||||
JsonObject o = el.getAsJsonObject();
|
||||
if (!o.has("classname")) continue;
|
||||
if (o.get("classname").getAsString().trim().toLowerCase(Locale.ROOT).equals(cn)) {
|
||||
foundName = (o.has("name") && !o.get("name").isJsonNull()) ? o.get("name").getAsString() : "";
|
||||
foundDesc = (o.has("description") && !o.get("description").isJsonNull()) ? o.get("description").getAsString() : "";
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean found = (foundName != null);
|
||||
this.client.sendResponse(new FurniEditorImportTextResultComposer(
|
||||
found, found ? foundName : "", found ? foundDesc : "", classname));
|
||||
LOGGER.info("FurniEditorImportTextEvent: admin {} import for classname '{}' (item {}) -> found={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), classname, itemId, found);
|
||||
}
|
||||
|
||||
/** Fetch the remote furnidata JSON with a TTL cache (serves stale on failure). */
|
||||
private static synchronized JsonObject fetchCached(String url) {
|
||||
long ttlMs;
|
||||
try {
|
||||
ttlMs = Long.parseLong(Emulator.getConfig().getValue("furni.editor.import.cache.ms", "600000"));
|
||||
} catch (Exception e) {
|
||||
ttlMs = 600000L;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (CACHE != null && url.equals(CACHE_URL) && (now - CACHE_TIME) < ttlMs) {
|
||||
return CACHE;
|
||||
}
|
||||
|
||||
try {
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(20))
|
||||
.header("User-Agent", "Arcturus-FurniEditor")
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (resp.statusCode() != 200) {
|
||||
LOGGER.warn("FurniEditorImportTextEvent: fetch {} returned HTTP {}", url, resp.statusCode());
|
||||
return CACHE; // serve stale if available
|
||||
}
|
||||
JsonObject root = JsonParser.parseString(resp.body()).getAsJsonObject();
|
||||
CACHE = root;
|
||||
CACHE_URL = url;
|
||||
CACHE_TIME = now;
|
||||
return root;
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("FurniEditorImportTextEvent: failed to fetch {}", url, e);
|
||||
return CACHE; // serve stale if available
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataEntry;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataLock;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataWriter;
|
||||
import com.eu.habbo.habbohotel.items.FurnitureTextProvider;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furniture.FurnitureDataReloadComposer;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Incoming handler 10048 — admin reverts a furni's furnidata to the last rotating backup.
|
||||
*
|
||||
* Flow: permission check → read item_id → resolve classname → under FurnidataLock:
|
||||
* FurnidataWriter.revertLastBackup → FurnitureTextProvider.reindexFromSource →
|
||||
* broadcast FurnitureDataReloadComposer (10047) → audit log → respond.
|
||||
*/
|
||||
public class FurniEditorRevertFurnidataEvent extends MessageHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FurniEditorRevertFurnidataEvent.class);
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
|
||||
// 1. Permission check
|
||||
if (!habbo.hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Read packet
|
||||
int itemId = this.packet.readInt();
|
||||
|
||||
if (itemId <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Resolve classname from item_id (reuse static helper from update handler)
|
||||
String classname = FurniEditorUpdateFurnidataEvent.classnameForItem(itemId);
|
||||
String classnameForLog = (classname != null) ? classname : "?";
|
||||
|
||||
// 4. Verify provider is configured
|
||||
FurnitureTextProvider provider =
|
||||
Emulator.getGameEnvironment().getFurnitureTextProvider();
|
||||
|
||||
if (provider == null || provider.getSource() == null) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Furnidata source not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
int adminId = habbo.getHabboInfo().getId();
|
||||
|
||||
// 5. Revert + reindex + broadcast under the shared lock
|
||||
boolean reverted;
|
||||
List<FurnidataEntry> delta;
|
||||
|
||||
FurnidataLock.LOCK.lock();
|
||||
try {
|
||||
FurnidataWriter writer = new FurnidataWriter(
|
||||
provider.getSource(),
|
||||
provider.isSourceDirectory(),
|
||||
provider.getMaxBytes(),
|
||||
3 /* backupKeep */
|
||||
);
|
||||
reverted = writer.revertLastBackup();
|
||||
if (!reverted) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No backup found to revert"));
|
||||
return;
|
||||
}
|
||||
|
||||
delta = provider.reindexFromSource();
|
||||
|
||||
if (!delta.isEmpty()) {
|
||||
int deltaCap = Integer.parseInt(
|
||||
Emulator.getConfig().getValue("items.furnidata.delta.cap", "500"));
|
||||
FurnitureDataReloadComposer composer = (delta.size() > deltaCap)
|
||||
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
|
||||
: new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
|
||||
broadcastToAll(composer);
|
||||
}
|
||||
} finally {
|
||||
FurnidataLock.LOCK.unlock();
|
||||
}
|
||||
|
||||
// 6. Audit log (outside lock — DB write, not latency-sensitive)
|
||||
FurnidataAuditLog.record(
|
||||
adminId,
|
||||
classnameForLog,
|
||||
"revert",
|
||||
"", // previous state unknown at this point
|
||||
"",
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
// 7. Respond success
|
||||
this.client.sendResponse(new FurniEditorResultComposer(true, "Furnidata reverted", itemId));
|
||||
LOGGER.info("FurniEditorRevertFurnidataEvent: admin {} reverted furnidata for classname '{}' (item {})",
|
||||
adminId, classnameForLog, itemId);
|
||||
}
|
||||
|
||||
private static void broadcastToAll(FurnitureDataReloadComposer composer) {
|
||||
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
|
||||
if (habbo.getClient() != null) {
|
||||
habbo.getClient().sendResponse(composer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
-5
@@ -27,6 +27,8 @@ public class FurniEditorSearchEvent extends MessageHandler {
|
||||
String query = this.packet.readString();
|
||||
String type = this.packet.readString();
|
||||
int page = this.packet.readInt();
|
||||
String sortField = this.packet.readString();
|
||||
String sortDir = this.packet.readString();
|
||||
|
||||
// Input validation
|
||||
if (query.length() > 100) {
|
||||
@@ -47,15 +49,17 @@ public class FurniEditorSearchEvent extends MessageHandler {
|
||||
try {
|
||||
int numericQuery = Integer.parseInt(query);
|
||||
isNumeric = true;
|
||||
String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
|
||||
whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)");
|
||||
params.add(numericQuery);
|
||||
params.add(numericQuery);
|
||||
params.add("%" + query + "%");
|
||||
params.add("%" + query + "%");
|
||||
params.add(likeQuery);
|
||||
params.add(likeQuery);
|
||||
} catch (NumberFormatException e) {
|
||||
String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
|
||||
whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)");
|
||||
params.add("%" + query + "%");
|
||||
params.add("%" + query + "%");
|
||||
params.add(likeQuery);
|
||||
params.add(likeQuery);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +68,53 @@ public class FurniEditorSearchEvent extends MessageHandler {
|
||||
params.add(type);
|
||||
}
|
||||
|
||||
// Extend search with furnidata display-name matches (server-authoritative names in JSON).
|
||||
// Appends: OR (LOWER(item_name) IN (?,?,...) [AND type=?])
|
||||
// Both branches carry their own type filter, so type scoping is preserved.
|
||||
// Params: [existing LIKE params] [existing type?] [furniCns...] [type again?]
|
||||
if (!query.isEmpty()) {
|
||||
java.util.List<String> furniCns = Emulator.getGameEnvironment()
|
||||
.getFurnitureTextProvider()
|
||||
.findClassnamesByName(query);
|
||||
if (!furniCns.isEmpty()) {
|
||||
// Build: OR (LOWER(item_name) IN (?,?,...) [AND type = ?])
|
||||
StringBuilder orBranch = new StringBuilder(" OR (LOWER(item_name) IN (");
|
||||
for (int i = 0; i < furniCns.size(); i++) {
|
||||
if (i > 0) orBranch.append(", ");
|
||||
orBranch.append('?');
|
||||
}
|
||||
orBranch.append(')');
|
||||
if (type != null && !type.isEmpty()) {
|
||||
orBranch.append(" AND type = ?");
|
||||
}
|
||||
orBranch.append(')');
|
||||
whereClause.append(orBranch);
|
||||
params.addAll(furniCns);
|
||||
if (type != null && !type.isEmpty()) {
|
||||
params.add(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a SAFE ORDER BY from the whitelisted sort field/direction
|
||||
// (column names are never taken from raw user input — injection-proof).
|
||||
String orderColumn;
|
||||
switch (sortField == null ? "" : sortField) {
|
||||
case "spriteId": orderColumn = "sprite_id"; break;
|
||||
case "itemName": orderColumn = "item_name"; break;
|
||||
case "publicName": orderColumn = "public_name"; break;
|
||||
case "type": orderColumn = "type"; break;
|
||||
case "interactionType": orderColumn = "interaction_type"; break;
|
||||
case "id":
|
||||
default: orderColumn = "id"; break;
|
||||
}
|
||||
String orderDir = "desc".equalsIgnoreCase(sortDir) ? "DESC" : "ASC";
|
||||
|
||||
// Count total
|
||||
int total = 0;
|
||||
String countSql = "SELECT COUNT(*) FROM items_base " + whereClause;
|
||||
String dataSql = "SELECT * FROM items_base " + whereClause + " ORDER BY id ASC LIMIT ? OFFSET ?";
|
||||
String dataSql = "SELECT * FROM items_base " + whereClause
|
||||
+ " ORDER BY " + orderColumn + " " + orderDir + ", id ASC LIMIT ? OFFSET ?";
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataEntry;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataLock;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataWriter;
|
||||
import com.eu.habbo.habbohotel.items.FurnitureTextProvider;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furniture.FurnitureDataReloadComposer;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Incoming handler 10046 — admin saves a furni name/description in the editor.
|
||||
*
|
||||
* Flow: permission check → rate-limit → resolve classname from item_id →
|
||||
* under FurnidataLock: FurnidataWriter.write → FurnitureTextProvider.reindexFromSource →
|
||||
* broadcast FurnitureDataReloadComposer (10047) → audit log → respond.
|
||||
*/
|
||||
public class FurniEditorUpdateFurnidataEvent extends MessageHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FurniEditorUpdateFurnidataEvent.class);
|
||||
|
||||
/** Rate-limit: min milliseconds between successive calls per admin user id. */
|
||||
private static final long RATE_LIMIT_MS = 1_000L;
|
||||
|
||||
/** Per-admin last-call timestamp map. */
|
||||
private static final Map<Integer, Long> LAST_CALL = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
|
||||
// 1. Permission check
|
||||
if (!habbo.hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Rate-limit per admin
|
||||
int adminId = habbo.getHabboInfo().getId();
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = LAST_CALL.get(adminId);
|
||||
if (last != null && (now - last) < RATE_LIMIT_MS) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Too many requests"));
|
||||
return;
|
||||
}
|
||||
LAST_CALL.put(adminId, now);
|
||||
|
||||
// 3. Read packet
|
||||
int itemId = this.packet.readInt();
|
||||
JsonObject json;
|
||||
try {
|
||||
json = JsonParser.parseString(this.packet.readString()).getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid JSON data"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemId <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
String name = json.has("name") ? json.get("name").getAsString() : null;
|
||||
String description = json.has("description") ? json.get("description").getAsString() : null;
|
||||
|
||||
if (name == null && description == null) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No name or description provided"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Resolve classname from item_id
|
||||
String classname = classnameForItem(itemId);
|
||||
if (classname == null) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Write + reindex + broadcast under the shared lock
|
||||
FurnitureTextProvider provider =
|
||||
Emulator.getGameEnvironment().getFurnitureTextProvider();
|
||||
|
||||
if (provider == null || provider.getSource() == null) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Furnidata source not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture old values (before write) for the audit log
|
||||
String oldName = provider.getName(classname);
|
||||
// description is not indexed in the provider — treat as empty string for audit
|
||||
String oldDesc = "";
|
||||
|
||||
// FurnidataWriter.write() calls FurnitureTextProvider.sanitize() internally;
|
||||
// pass the raw values here and use them also for the audit log.
|
||||
String safeName = (name != null) ? name : "";
|
||||
String safeDesc = (description != null) ? description : "";
|
||||
|
||||
boolean written;
|
||||
List<FurnidataEntry> delta;
|
||||
|
||||
FurnidataLock.LOCK.lock();
|
||||
try {
|
||||
FurnidataWriter writer = new FurnidataWriter(
|
||||
provider.getSource(),
|
||||
provider.isSourceDirectory(),
|
||||
provider.getMaxBytes(),
|
||||
3 /* backupKeep */
|
||||
);
|
||||
written = writer.write(classname, safeName, safeDesc);
|
||||
if (!written) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Classname not found in furnidata"));
|
||||
return;
|
||||
}
|
||||
|
||||
delta = provider.reindexFromSource();
|
||||
|
||||
if (!delta.isEmpty()) {
|
||||
int deltaCap = Integer.parseInt(
|
||||
Emulator.getConfig().getValue("items.furnidata.delta.cap", "500"));
|
||||
FurnitureDataReloadComposer composer = (delta.size() > deltaCap)
|
||||
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
|
||||
: new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
|
||||
broadcastToAll(composer);
|
||||
}
|
||||
} finally {
|
||||
FurnidataLock.LOCK.unlock();
|
||||
}
|
||||
|
||||
// 5b. Auto-mirror the new display name into items_base.public_name (DB) so the
|
||||
// server-side fallback (Item.getFullName) and the editor's read-only
|
||||
// "Public Name" field stay in sync with the furnidata edit. Only when a
|
||||
// name was actually supplied (description-only edits must not blank it).
|
||||
// Kept outside FurnidataLock (independent DB write, like the audit log).
|
||||
if (name != null) {
|
||||
try (Connection c = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement st = c.prepareStatement("UPDATE items_base SET public_name = ? WHERE id = ?")) {
|
||||
st.setString(1, FurnitureTextProvider.sanitize(safeName));
|
||||
st.setInt(2, itemId);
|
||||
st.executeUpdate();
|
||||
// Refresh the in-memory Item cache (Item.fullName) in place — no restart needed.
|
||||
Emulator.getGameEnvironment().getItemManager().loadItems();
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to mirror furnidata name into items_base.public_name for item {}", itemId, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Audit log (outside lock — DB write, not latency-sensitive)
|
||||
FurnidataAuditLog.record(
|
||||
adminId,
|
||||
classname,
|
||||
"edit",
|
||||
oldName != null ? oldName : "",
|
||||
FurnitureTextProvider.sanitize(safeName),
|
||||
oldDesc,
|
||||
FurnitureTextProvider.sanitize(safeDesc)
|
||||
);
|
||||
|
||||
// 7. Respond success
|
||||
this.client.sendResponse(new FurniEditorResultComposer(true, "Furnidata updated", itemId));
|
||||
LOGGER.info("FurniEditorUpdateFurnidataEvent: admin {} updated furnidata for classname '{}' (item {})",
|
||||
adminId, classname, itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the item_name (classname) from items_base for a given item id.
|
||||
* Kept static so FurniEditorRevertFurnidataEvent can reuse it.
|
||||
*
|
||||
* @return the classname string, or {@code null} if not found or on error.
|
||||
*/
|
||||
public static String classnameForItem(int itemId) {
|
||||
try (Connection c = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement st = c.prepareStatement("SELECT item_name FROM items_base WHERE id = ?")) {
|
||||
st.setInt(1, itemId);
|
||||
try (ResultSet rs = st.executeQuery()) {
|
||||
if (rs.next()) return rs.getString("item_name");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("classnameForItem: failed to query items_base for id {}", itemId, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void broadcastToAll(FurnitureDataReloadComposer composer) {
|
||||
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
|
||||
if (habbo.getClient() != null) {
|
||||
habbo.getClient().sendResponse(composer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
|
||||
public final class FurnidataAuditLog {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FurnidataAuditLog.class);
|
||||
private FurnidataAuditLog() {}
|
||||
|
||||
public static void record(int userId, String classname, String action,
|
||||
String oldName, String newName, String oldDesc, String newDesc) {
|
||||
try (Connection c = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement st = c.prepareStatement(
|
||||
"INSERT INTO furnidata_edit_log (user_id, classname, action, old_name, new_name, old_description, new_description, timestamp) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?)")) {
|
||||
st.setInt(1, userId);
|
||||
st.setString(2, classname);
|
||||
st.setString(3, action);
|
||||
st.setString(4, oldName == null ? "" : oldName);
|
||||
st.setString(5, newName == null ? "" : newName);
|
||||
st.setString(6, oldDesc == null ? "" : oldDesc);
|
||||
st.setString(7, newDesc == null ? "" : newDesc);
|
||||
st.setInt(8, Emulator.getIntUnixTimestamp());
|
||||
st.executeUpdate();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to write furnidata_edit_log", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,15 @@ public class MachineIDEvent extends MessageHandler {
|
||||
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
|
||||
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
|
||||
Emulator.getThreading().run(this.client.getHabbo());
|
||||
|
||||
// The fingerprint can arrive AFTER login (UniqueID is sent right after the
|
||||
// SSO ticket), so Habbo.connect() may have skipped the MAC-ban check for
|
||||
// lack of a machineId. Enforce it now that the fingerprint is known.
|
||||
if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
|
||||
Emulator.getGameServer().getGameClientManager().forceDisposeClient(this.client);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("Setting client MachineId to {}", storedMachineId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -306,7 +306,7 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
Emulator.getPluginManager().fireEvent(userLoginEvent);
|
||||
|
||||
if(userLoginEvent.isCancelled()) {
|
||||
Emulator.getGameServer().getGameClientManager().disposeClient(this.client);
|
||||
Emulator.getGameServer().getGameClientManager().forceDisposeClient(this.client);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -12,6 +12,7 @@ import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.give_credits";
|
||||
private static final int MAX_GRANT = 1_000_000_000;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
@@ -27,7 +28,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
int userId = this.packet.readInt();
|
||||
int amount = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || amount == 0) {
|
||||
if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
@@ -38,6 +39,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
// giveCredits already pushes UserCreditsComposer and persists via the
|
||||
// standard HabboInfo write path; nothing extra needed for the online branch.
|
||||
online.giveCredits(amount);
|
||||
this.audit(userId, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
@@ -57,6 +59,15 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
this.audit(userId, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
|
||||
private void audit(int userId, int amount) {
|
||||
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||
this.client.getHabbo().getHabboInfo().getId(),
|
||||
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||
ACTION_KEY, userId, "amount=" + amount,
|
||||
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -18,6 +18,7 @@ import java.sql.SQLException;
|
||||
*/
|
||||
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
private static final int CURRENCY_DUCKETS = 0;
|
||||
private static final int MAX_GRANT = 1_000_000_000;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
@@ -36,7 +37,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
|
||||
String actionKey = "user.give_currency_" + currencyType;
|
||||
|
||||
if (userId <= 0 || amount == 0) {
|
||||
if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
@@ -52,6 +53,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
online.givePoints(currencyType, amount);
|
||||
}
|
||||
|
||||
this.audit(actionKey, userId, currencyType, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
@@ -69,6 +71,15 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
this.audit(actionKey, userId, currencyType, amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
}
|
||||
|
||||
private void audit(String actionKey, int userId, int currencyType, int amount) {
|
||||
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||
this.client.getHabbo().getHabboInfo().getId(),
|
||||
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||
actionKey, userId, "type=" + currencyType + " amount=" + amount,
|
||||
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class HousekeepingSearchRoomsEvent extends MessageHandler {
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, exactMatch ? query : query + "%");
|
||||
statement.setString(1, exactMatch ? query : com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||
statement.setInt(2, limit);
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
|
||||
+44
-2
@@ -11,6 +11,7 @@ import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||
@@ -44,6 +45,43 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||
|
||||
Rank rank = permissions.getRank(rankId);
|
||||
|
||||
// Rank-ceiling guard: an operator must never be able to grant a rank
|
||||
// above their own, nor modify a user who already outranks them. This
|
||||
// mirrors GiveRankCommand and prevents privilege escalation through
|
||||
// the housekeeping path (including self-promotion).
|
||||
int operatorRankId = this.client.getHabbo().getHabboInfo().getRank().getId();
|
||||
|
||||
if (rank.getId() > operatorRankId) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
int targetRankId;
|
||||
if (online != null) {
|
||||
targetRankId = online.getHabboInfo().getRank().getId();
|
||||
} else {
|
||||
targetRankId = 0;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT rank FROM users WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, userId);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) {
|
||||
targetRankId = set.getInt("rank");
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetRankId > operatorRankId) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Persist for the offline path. Online users get their in-memory
|
||||
// HabboInfo.rank rebound below so server-side hasPermission()
|
||||
// checks land on the new permission set without a relogin.
|
||||
@@ -57,8 +95,6 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
online.getHabboInfo().setRank(rank);
|
||||
// Ship the refreshed permissions snapshot — same payload the
|
||||
@@ -66,6 +102,12 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||
online.getClient().sendResponse(new UserPermissionsComposer(online));
|
||||
}
|
||||
|
||||
com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
|
||||
this.client.getHabbo().getHabboInfo().getId(),
|
||||
this.client.getHabbo().getHabboInfo().getUsername(),
|
||||
ACTION_KEY, userId, "rankId=" + rankId,
|
||||
this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@ package com.eu.habbo.messages.incoming.modtool;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ScripterManager;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.modtool.ModToolUserChatlogComposer;
|
||||
@@ -12,7 +13,11 @@ public class ModToolRequestUserChatlogEvent extends MessageHandler {
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
|
||||
int userId = this.packet.readInt();
|
||||
String username = HabboManager.getOfflineHabboInfo(userId).getUsername();
|
||||
HabboInfo habboInfo = HabboManager.getOfflineHabboInfo(userId);
|
||||
if (habboInfo == null) {
|
||||
return;
|
||||
}
|
||||
String username = habboInfo.getUsername();
|
||||
|
||||
this.client.sendResponse(new ModToolUserChatlogComposer(Emulator.getGameEnvironment().getModToolManager().getUserRoomVisitsAndChatlogs(userId), userId, username));
|
||||
} else {
|
||||
|
||||
+12
@@ -5,6 +5,13 @@ import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.navigator.NewNavigatorSavedSearchesComposer;
|
||||
|
||||
public class AddSavedSearchEvent extends MessageHandler {
|
||||
private static final int MAX_SAVED_SEARCHES = 50;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
String searchCode = this.packet.readString();
|
||||
@@ -13,6 +20,11 @@ public class AddSavedSearchEvent extends MessageHandler {
|
||||
if (searchCode.length() > 255) searchCode = searchCode.substring(0, 255);
|
||||
if (filter.length() > 255) filter = filter.substring(0, 255);
|
||||
|
||||
if (this.client.getHabbo().getHabboInfo().getSavedSearches().size() >= MAX_SAVED_SEARCHES) {
|
||||
this.client.sendResponse(new NewNavigatorSavedSearchesComposer(this.client.getHabbo().getHabboInfo().getSavedSearches()));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.getHabbo().getHabboInfo().addSavedSearch(new NavigatorSavedSearch(searchCode, filter));
|
||||
|
||||
this.client.sendResponse(new NewNavigatorSavedSearchesComposer(this.client.getHabbo().getHabboInfo().getSavedSearches()));
|
||||
|
||||
+5
@@ -5,6 +5,11 @@ import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.navigator.NewNavigatorSavedSearchesComposer;
|
||||
|
||||
public class DeleteSavedSearchEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int searchId = this.packet.readInt();
|
||||
|
||||
+18
-18
@@ -15,12 +15,20 @@ import java.util.*;
|
||||
|
||||
public class RequestNewNavigatorRoomsEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RequestNewNavigatorRoomsEvent.class);
|
||||
private static final int MAX_VIEW_LENGTH = 32;
|
||||
private static final int MAX_QUERY_LENGTH = 64;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
String view = this.packet.readString();
|
||||
String query = this.packet.readString();
|
||||
|
||||
if (view.length() > MAX_VIEW_LENGTH) return;
|
||||
if (query.length() > MAX_QUERY_LENGTH) query = query.substring(0, MAX_QUERY_LENGTH);
|
||||
if (view.equals("query")) view = "hotel_view";
|
||||
if (view.equals("groups")) view = "hotel_view";
|
||||
|
||||
@@ -43,7 +51,7 @@ public class RequestNewNavigatorRoomsEvent extends MessageHandler {
|
||||
NavigatorFilterField field = Emulator.getGameEnvironment().getNavigatorManager().filterSettings.get(filterField);
|
||||
if (filter != null) {
|
||||
if (query.contains(":")) {
|
||||
String[] parts = query.split(":");
|
||||
String[] parts = query.split(":", 2);
|
||||
|
||||
if (parts.length > 1) {
|
||||
filterField = parts[0];
|
||||
@@ -53,6 +61,7 @@ public class RequestNewNavigatorRoomsEvent extends MessageHandler {
|
||||
if (!Emulator.getGameEnvironment().getNavigatorManager().filterSettings.containsKey(filterField)) {
|
||||
filterField = "anything";
|
||||
}
|
||||
part = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,28 +104,19 @@ public class RequestNewNavigatorRoomsEvent extends MessageHandler {
|
||||
rooms.addAll(searchResultList.rooms);
|
||||
resultLists.add(new SearchResultList(searchResultList.order, searchResultList.code, searchResultList.query, searchResultList.action, searchResultList.mode, searchResultList.hidden, rooms, searchResultList.filter, searchResultList.showInvisible, searchResultList.displayOrder, searchResultList.categoryOrder));
|
||||
}
|
||||
if ("group".equals(filterField)) {
|
||||
final String needle = part.toLowerCase();
|
||||
for (SearchResultList list : resultLists) {
|
||||
list.rooms.removeIf(room -> !room.belongsToGuild()
|
||||
|| (!needle.isEmpty() && !room.getGuildName().toLowerCase().contains(needle)));
|
||||
}
|
||||
}
|
||||
filter.filter(field.field, part, resultLists);
|
||||
resultLists = toQueryResults(resultLists);
|
||||
this.client.sendResponse(new NewNavigatorSearchResultsComposer(view, query, resultLists));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
}
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
|
||||
List<SearchResultList> resultLists = new ArrayList<>(filter.getResult(this.client.getHabbo(), field, part, category != null ? category.getId() : -1));
|
||||
filter.filter(field.field, part, resultLists);
|
||||
|
||||
Collections.sort(resultLists);
|
||||
this.client.sendResponse(new NewNavigatorSearchResultsComposer(view, query, resultLists));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.error("Caught exception", e);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private ArrayList<SearchResultList> toQueryResults(List<SearchResultList> resultLists) {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public class RoomRequestBannedUsersEvent extends MessageHandler {
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (!room.hasRights(this.client.getHabbo()) || !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return;
|
||||
if (!room.hasRights(this.client.getHabbo()) && !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return;
|
||||
|
||||
this.client.sendResponse(new RoomBannedUsersComposer(room));
|
||||
|
||||
|
||||
+5
@@ -25,6 +25,11 @@ public class ToggleFloorItemEvent extends MessageHandler {
|
||||
|
||||
private static HashSet<String> PET_BOXES = new HashSet<>(Arrays.asList("val11_present", "gnome_box", "leprechaun_box", "velociraptor_egg", "pterosaur_egg", "petbox_epic"));
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
try {
|
||||
|
||||
+5
@@ -9,6 +9,11 @@ import com.eu.habbo.plugin.Event;
|
||||
import com.eu.habbo.plugin.events.furniture.FurnitureToggleEvent;
|
||||
|
||||
public class ToggleWallItemEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user