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
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e59dca192 | |||
| 1109d53720 | |||
| f12363a5b7 | |||
| 7d4ffec74e | |||
| 281fede58c | |||
| edf152485b | |||
| 18a1bfbe90 | |||
| 7c32bbfd2d | |||
| 4eae206b64 | |||
| 155b2202c7 | |||
| 10c291eb9f | |||
| 349a8c727e | |||
| 68f2b71d14 | |||
| 69a6c0d060 | |||
| 885bdca0c4 | |||
| db035294a7 | |||
| 3216ba1df6 | |||
| c9a47b1fac | |||
| 8d6b969d75 | |||
| b9723e0298 | |||
| c4aae676b2 | |||
| 7624d3fbc3 | |||
| 585f4dd3aa | |||
| afa114d511 | |||
| e9129576a9 | |||
| 0aadd01493 | |||
| 9d98fbf9ee | |||
| b38274e134 | |||
| 02ab30180c | |||
| da63439d53 | |||
| bf1a29a6e8 | |||
| 6391d721ff | |||
| dfea6bcf83 | |||
| a7f207bb76 | |||
| b7915884b6 | |||
| 478f7bdba0 | |||
| c255f1e1b4 | |||
| 9c831a9da4 | |||
| 08d1ae97a7 | |||
| f8fe1e3e22 | |||
| be77cdf4aa | |||
| 1ba2e43d4d | |||
| 8dd5155562 | |||
| 4f4f581371 | |||
| 9705b3e42a | |||
| e626a7fc50 | |||
| d6ebb632e6 | |||
| 014ca9ca48 | |||
| d189d66f9e | |||
| c272a36cc5 | |||
| 1d6e05ee57 | |||
| ea44771d69 | |||
| 1da783aff9 | |||
| e772686c4b | |||
| a00f7b01f5 | |||
| 6b4089cace | |||
| 9ea7acf05c | |||
| bab43af41e | |||
| 10a2b2b872 | |||
| 458b37dbed | |||
| 55b38e7b85 | |||
| 4a96c5baaf | |||
| 539c5b5b96 | |||
| 7b7154e68f | |||
| 4aabb738a3 | |||
| 691dc42627 | |||
| 226873c1fb | |||
| a06a204b39 | |||
| e213609609 | |||
| 44d38b8661 | |||
| ccadb81970 | |||
| 0a3a940946 | |||
| 4613fbe80c | |||
| 9328f4a355 | |||
| da8b947ddf | |||
| b9658d0407 | |||
| 68d3731393 | |||
| 4ef4ed1a96 | |||
| c20e273a2c | |||
| dac09e92d1 | |||
| fbf979419e | |||
| 6126c35779 | |||
| a1749c9eda | |||
| 525c124fa5 | |||
| 57087a31f2 | |||
| c4b3295a45 | |||
| 418c753e6c | |||
| 8419f11883 | |||
| 1a0d783ff7 | |||
| 655e039df7 | |||
| 7726691cde |
@@ -322,13 +322,6 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
|
||||
PRIMARY KEY (`key_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`word` VARCHAR(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_custom_prefix_blacklist_word` (`word`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
|
||||
('max_length', '15'),
|
||||
('min_rank_to_buy', '1'),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`, `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) VALUES ('acc_housekeeping', '1', 'Allow housekeeping in the client', '0', '0', '0', '0', '0', '0', '1');
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `housekeeping_log` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`timestamp` INT NOT NULL,
|
||||
`actor_id` INT NOT NULL,
|
||||
`actor_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||
`target_id` INT NOT NULL DEFAULT 0,
|
||||
`target_label` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`action` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`detail` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`success` TINYINT NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `timestamp` (`timestamp`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,70 @@
|
||||
ALTER TABLE `bots`
|
||||
MODIFY COLUMN `type` ENUM('generic','visitor_log','bartender','weapons_dealer','frank')
|
||||
NOT NULL DEFAULT 'generic';
|
||||
|
||||
INSERT INTO `permission_definitions`
|
||||
(`permission_key`, `max_value`, `comment`,
|
||||
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
|
||||
VALUES
|
||||
('acc_bot_frank', 1, 'Required to purchase the Frank mascot bot from the catalog.',
|
||||
0, 0, 0, 0, 0, 0, 1)
|
||||
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `bot_chat_responses` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`bot_type` VARCHAR(32) NOT NULL,
|
||||
`keys` VARCHAR(255) NOT NULL COMMENT 'semicolon-separated trigger words',
|
||||
`responses` TEXT NOT NULL COMMENT 'newline-separated replies; bot picks one at random',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `bot_type` (`bot_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
|
||||
('frank', '__door_triggers', 'show me the door\nkick me\ni want to leave\nlet me out'),
|
||||
('frank', '__door_lines', 'Right this way - mind the step!\nAnd out you go. Come back soon!\nAllow me to escort you to the exit.\nThere''s the door. Farewell, true believer!'),
|
||||
('frank', '__busy_whisper', 'Sorry, I am currently busy. Please wait until I am available.'),
|
||||
('frank', 'frank', 'Hello, I''m Frank! Welcome to Habbo.'),
|
||||
('frank', 'help', 'What do you need help with?'),
|
||||
('frank', 'thanks;thank you', 'Just doing my job, true believer!'),
|
||||
('frank', 'new', 'Welcome to Habbo! I hope you have a great time here.'),
|
||||
('frank', 'rooms', 'Looking for somewhere fun? Try the Navigator - thousands of rooms to explore!'),
|
||||
('frank', 'sulake', 'Sulake is the company behind Habbo. Take a look: https://www.sulake.com'),
|
||||
('frank', 'vip;hc', 'VIP gets you more outfits, more furni, more everything. Worth it!'),
|
||||
('frank', 'music', 'Snoop Dogg, Frank Sinatra and a little Beethoven on Sundays.'),
|
||||
('frank', 'movie', 'I''m a Casablanca man. Black and white films are an underrated art.'),
|
||||
('frank', 'game', 'Battleship. Always Battleship.'),
|
||||
('frank', 'snowstorm', 'Honestly? I''m terrible at Snowstorm. Don''t tell anyone.'),
|
||||
('frank', 'furni', 'Best furniture maker in town - hands down, the folks at Sulake.'),
|
||||
('frank', 'animal;cat;pet','I have a cat called Mr. Whiskers. He runs the place, really.'),
|
||||
('frank', 'miranda', 'Miranda. The love of my life. Don''t get me started.'),
|
||||
('frank', 'frank black', 'Named after the man himself. Frank Black is a hero of mine.'),
|
||||
('frank', 'life', 'Life is like a bowl of popcorn - warm, salty and buttery.'),
|
||||
('frank', 'job;work', 'I''m sure you can find work in one of the guest rooms!'),
|
||||
('frank', 'snouthill', 'Snouthill... so many memories.'),
|
||||
('frank', 'wife', 'I had a wife once. She broke my stereo.'),
|
||||
('frank', 'baseball', 'Oh, I used to love to go down to the old ball park and watch Christy Mathewson and Honus Wagner at bat.'),
|
||||
('frank', 'mark', 'I don''t trust Mark.'),
|
||||
('frank', 'vietnam', 'Vietnam? Don''t ask. Worst trip of my life.'),
|
||||
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
|
||||
|
||||
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
|
||||
('sunflower', 1002),
|
||||
('cola;habbo cola', 19),
|
||||
('rose', 1000),
|
||||
('book', 1003),
|
||||
('tea', 27),
|
||||
('coffee', 8),
|
||||
('migraine;headache;pills', 1015),
|
||||
('radioactive liquid;radioactive', 30),
|
||||
('turkey;can of turkey', 70);
|
||||
|
||||
-- VERY IMPORTANT !!!!
|
||||
-- First check if the items_base ID and catalog_items ID is not in use !
|
||||
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
|
||||
|
||||
INSERT IGNORE INTO `items_base` (`id`, `sprite_id`, `item_name`, `public_name`, `width`, `length`, `stack_height`, `allow_stack`, `allow_sit`, `allow_lay`, `allow_walk`, `allow_gift`, `allow_trade`, `allow_recycle`, `allow_marketplace_sell`, `allow_inventory_stack`, `type`, `interaction_type`, `interaction_modes_count`, `vending_ids`, `multiheight`, `customparams`)
|
||||
VALUES (19001, 0, 'bot_frank', 'Frank', 1, 1, 0.00, '0', '0', '0', '1', '0', '0', '0', '0', '0', 'r', 'default', 1, '0', '0', '0');
|
||||
|
||||
INSERT IGNORE INTO `catalog_items` (`item_ids`, `page_id`, `offer_id`, `catalog_name`, `cost_credits`, `cost_points`, `points_type`, `amount`, `extradata`)
|
||||
VALUES ('19001', 8, 19001, 'Frank', 0, 0, 0, 1, 'name:Frank;motto:Welcome to Habbo!;figure:hr-3499-33.sh-290-90.ch-3971-72-73.lg-270-73.hd-205-1-1.fa-1206-67.ha-3409-73-72;gender:m');
|
||||
@@ -0,0 +1,89 @@
|
||||
ALTER TABLE `rooms`
|
||||
ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `soundboard_sounds` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client
|
||||
`url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges)
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Fortune Wheel — tables
|
||||
-- ----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `wheel_prizes` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`type` VARCHAR(16) NOT NULL DEFAULT 'nothing', -- item | badge | credits | points | spin | nothing
|
||||
`value` VARCHAR(64) NOT NULL DEFAULT '', -- item: base item id ; badge: badge code ; others: unused
|
||||
`amount` INT(11) NOT NULL DEFAULT 1, -- item qty / credits / points / extra spins
|
||||
`points_type` INT(11) NOT NULL DEFAULT 5, -- for type=points (diamond default 5)
|
||||
`weight` INT(11) NOT NULL DEFAULT 1, -- relative probability
|
||||
`label` VARCHAR(64) NOT NULL DEFAULT '', -- slice label override (optional)
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wheel_user_state` (
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`free_spins` INT(11) NOT NULL DEFAULT 0, -- remaining free spins for the current day
|
||||
`extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins
|
||||
`last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400)
|
||||
PRIMARY KEY (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wheel_recent_wins` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`username` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`look` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`prize_label` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`won_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('wheel.free_spins_per_day', '1', 'Fortune wheel: free spins granted each day.'),
|
||||
('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.'),
|
||||
('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).')
|
||||
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
|
||||
|
||||
INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`)
|
||||
SELECT `type`, `amount`, `points_type`, `weight`, `label`, `sort_order`
|
||||
FROM (
|
||||
SELECT 'points' AS `type`, 25 AS `amount`, 5 AS `points_type`, 20 AS `weight`, '25 diamonds' AS `label`, 1 AS `sort_order`
|
||||
UNION ALL SELECT 'points', 50, 5, 12, '50 diamonds', 2
|
||||
UNION ALL SELECT 'points', 200, 5, 3, '200 diamonds', 3
|
||||
UNION ALL SELECT 'credits', 100, 0, 15, '100 credits', 4
|
||||
UNION ALL SELECT 'spin', 1, 0, 15, '1 Extra spin', 5
|
||||
UNION ALL SELECT 'spin', 2, 0, 6, '2 Extra spins', 6
|
||||
UNION ALL SELECT 'nothing', 0, 0, 29, 'Oh to bad!', 7
|
||||
) AS seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `wheel_prizes`);
|
||||
|
||||
INSERT IGNORE INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
|
||||
VALUES (
|
||||
'acc_wheeladmin',
|
||||
1,
|
||||
'Allows opening the Fortune Wheel prize editor (FortuneWheelSettingsView) to add/edit prize slices. Gated server-side by the same key.'
|
||||
);
|
||||
|
||||
SET @cols := NULL;
|
||||
SELECT GROUP_CONCAT(CONCAT('dst.`', `column_name`, '` = src.`', `column_name`, '`') SEPARATOR ', ')
|
||||
INTO @cols
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'permission_definitions'
|
||||
AND `column_name` REGEXP '^rank_[0-9]+$';
|
||||
|
||||
SET @sql := CONCAT(
|
||||
'UPDATE `permission_definitions` dst ',
|
||||
'JOIN `permission_definitions` src ON src.`permission_key` = ''acc_ads_background'' ',
|
||||
'SET ', @cols, ' ',
|
||||
'WHERE dst.`permission_key` = ''acc_wheeladmin'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
|
||||
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');
|
||||
|
||||
|
||||
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`;
|
||||
@@ -63,15 +63,6 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
|
||||
PRIMARY KEY (`key_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5. Blacklist table
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`word` VARCHAR(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_word` (`word`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- Schema upgrades for existing installations
|
||||
@@ -296,14 +287,6 @@ INSERT IGNORE INTO `custom_prefixes_catalog`
|
||||
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
|
||||
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
|
||||
|
||||
-- ============================================================
|
||||
-- Example blacklist entries
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
|
||||
('admin'),
|
||||
('staff'),
|
||||
('mod'),
|
||||
('owner');
|
||||
|
||||
-- ============================================================
|
||||
-- Notes
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.eu.habbo</groupId>
|
||||
<artifactId>Habbo</artifactId>
|
||||
<version>4.2.17</version>
|
||||
<version>4.2.32</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -38,6 +38,7 @@
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.eu.habbo.Emulator</mainClass>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
|
||||
@@ -39,12 +39,23 @@ public final class Emulator {
|
||||
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
|
||||
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
|
||||
|
||||
// Fallback version, only used when running outside a packaged jar (e.g. from
|
||||
// the IDE). In production the version comes from the jar manifest below.
|
||||
public final static int MAJOR = 4;
|
||||
public final static int MINOR = 1;
|
||||
public final static int BUILD = 0;
|
||||
public final static String PREVIEW = "";
|
||||
|
||||
public static final String version = "Arcturus Morningstar" + " " + MAJOR + "." + MINOR + "." + BUILD + " " + PREVIEW;
|
||||
// Tied to the Maven project version: read from the jar manifest
|
||||
// (Implementation-Version = ${project.version}, see pom assembly plugin).
|
||||
private static String resolveVersionNumber() {
|
||||
String implementation = Emulator.class.getPackage().getImplementationVersion();
|
||||
if (implementation != null && !implementation.isEmpty()) return implementation;
|
||||
String fallback = MAJOR + "." + MINOR + "." + BUILD;
|
||||
return PREVIEW.isEmpty() ? fallback : fallback + " " + PREVIEW;
|
||||
}
|
||||
|
||||
public static final String version = "Arcturus Morningstar Extended " + resolveVersionNumber();
|
||||
private static final String logo =
|
||||
"\n" +
|
||||
"███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" +
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.eu.habbo.habbohotel.achievements.AchievementManager;
|
||||
import com.eu.habbo.habbohotel.bots.BotManager;
|
||||
import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
||||
import com.eu.habbo.habbohotel.wheel.WheelManager;
|
||||
import com.eu.habbo.habbohotel.soundboard.SoundboardManager;
|
||||
import com.eu.habbo.habbohotel.mentions.MentionManager;
|
||||
import com.eu.habbo.habbohotel.commands.CommandHandler;
|
||||
import com.eu.habbo.habbohotel.crafting.CraftingManager;
|
||||
import com.eu.habbo.habbohotel.guides.GuideManager;
|
||||
@@ -64,6 +67,9 @@ public class GameEnvironment {
|
||||
private GoogleTranslateManager googleTranslateManager;
|
||||
private CustomBadgeManager customBadgeManager;
|
||||
private InfostandBackgroundManager infostandBackgroundManager;
|
||||
private WheelManager wheelManager;
|
||||
private SoundboardManager soundboardManager;
|
||||
private MentionManager mentionManager;
|
||||
|
||||
public void load() throws Exception {
|
||||
LOGGER.info("GameEnvironment -> Loading...");
|
||||
@@ -93,6 +99,9 @@ public class GameEnvironment {
|
||||
this.googleTranslateManager = new GoogleTranslateManager();
|
||||
this.customBadgeManager = new CustomBadgeManager();
|
||||
this.infostandBackgroundManager = new InfostandBackgroundManager();
|
||||
this.wheelManager = new WheelManager();
|
||||
this.soundboardManager = new SoundboardManager();
|
||||
this.mentionManager = new MentionManager();
|
||||
|
||||
this.roomManager.loadPublicRooms();
|
||||
this.navigatorManager.loadNavigator();
|
||||
@@ -156,6 +165,14 @@ public class GameEnvironment {
|
||||
return this.catalogManager;
|
||||
}
|
||||
|
||||
public WheelManager getWheelManager() {
|
||||
return this.wheelManager;
|
||||
}
|
||||
|
||||
public SoundboardManager getSoundboardManager() {
|
||||
return this.soundboardManager;
|
||||
}
|
||||
|
||||
public HotelViewManager getHotelViewManager() {
|
||||
return this.hotelViewManager;
|
||||
}
|
||||
@@ -188,6 +205,10 @@ public class GameEnvironment {
|
||||
return this.petManager;
|
||||
}
|
||||
|
||||
public MentionManager getMentionManager() {
|
||||
return this.mentionManager;
|
||||
}
|
||||
|
||||
public AchievementManager getAchievementManager() {
|
||||
return this.achievementManager;
|
||||
}
|
||||
|
||||
@@ -189,11 +189,7 @@ public class Bot implements Runnable {
|
||||
int timeOut = Emulator.getRandom().nextInt(20) * 2;
|
||||
this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp());
|
||||
}
|
||||
}/* else {
|
||||
for (RoomTile t : this.room.getLayout().getTilesAround(this.room.getLayout().getTile(this.getRoomUnit().getX(), this.getRoomUnit().getY()))) {
|
||||
WiredManager.handle(WiredTriggerType.BOT_REACHED_STF, this.roomUnit, this.room, this.room.getItemsAt(t).toArray());
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) {
|
||||
@@ -218,7 +214,7 @@ public class Bot implements Runnable {
|
||||
} else {
|
||||
this.lastChatIndex++;
|
||||
if (this.lastChatIndex >= this.chatLines.size()) {
|
||||
this.lastChatIndex = 0; // start from scratch :-3
|
||||
this.lastChatIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,9 +306,6 @@ public class Bot implements Runnable {
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
this.needsUpdate = true;
|
||||
|
||||
//if(this.room != null)
|
||||
//this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose());
|
||||
}
|
||||
|
||||
public String getMotto() {
|
||||
@@ -539,5 +532,28 @@ public class Bot implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11};
|
||||
|
||||
public static final int ACTION_ROTATE = 11;
|
||||
|
||||
private static final long MIN_OWNER_ACTION_INTERVAL_MS = 200L;
|
||||
|
||||
private volatile long lastOwnerActionAt;
|
||||
|
||||
public short[] getOwnerActionIds() {
|
||||
return DEFAULT_OWNER_ACTION_IDS;
|
||||
}
|
||||
|
||||
public synchronized boolean tryAcquireOwnerActionSlot() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - this.lastOwnerActionAt < MIN_OWNER_ACTION_INTERVAL_MS) {
|
||||
return false;
|
||||
}
|
||||
this.lastOwnerActionAt = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onPostOwnerAction(int actionId) {
|
||||
// no-op default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ public class BotManager {
|
||||
addBotDefinition("generic", Bot.class);
|
||||
addBotDefinition("bartender", ButlerBot.class);
|
||||
addBotDefinition("visitor_log", VisitorBot.class);
|
||||
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
|
||||
|
||||
this.reload();
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
package com.eu.habbo.habbohotel.bots;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.*;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer;
|
||||
import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FrankBot extends ButlerBot {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
|
||||
public static final String BOT_TYPE = "frank";
|
||||
public static final String PERMISSION_USE = "acc_bot_frank";
|
||||
private static final String KEY_DOOR_LINES = "__door_lines";
|
||||
private static final String KEY_BUSY_WHISPER = "__busy_whisper";
|
||||
private static final String KEY_DOOR_TRIGGERS = "__door_triggers";
|
||||
private static final List<String> DEFAULT_DOOR_LINES = List.of(
|
||||
"Right this way - mind the step!",
|
||||
"And out you go. Come back soon!",
|
||||
"Allow me to escort you to the exit.",
|
||||
"There's the door. Farewell, true believer!"
|
||||
);
|
||||
private static final String DEFAULT_BUSY_WHISPER =
|
||||
"Sorry, I am currently busy. Please wait until I am available.";
|
||||
private static final Pattern DEFAULT_DOOR_PATTERN = Pattern.compile(
|
||||
"\\b(show me the door|kick me|i want to leave|let me out)\\b");
|
||||
|
||||
private static final ConcurrentHashMap<Pattern, List<String>> chatResponses = new ConcurrentHashMap<>();
|
||||
private static volatile List<String> doorLines = DEFAULT_DOOR_LINES;
|
||||
private static volatile String busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
private static volatile Pattern doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final int MAX_CHAT_KEYWORDS = 256;
|
||||
private static final int MAX_DOOR_TRIGGERS = 32;
|
||||
private static final int MAX_MESSAGE_LEN = 256;
|
||||
private static final long BUSY_WHISPER_COOLDOWN_MS = 5000L;
|
||||
|
||||
private volatile RoomTile homeTile;
|
||||
private volatile RoomUserRotation homeRotation;
|
||||
private final AtomicBoolean busy = new AtomicBoolean(false);
|
||||
private final AtomicBoolean returnScheduled = new AtomicBoolean(false);
|
||||
private final ConcurrentHashMap<Integer, Long> lastBusyWhisperAt = new ConcurrentHashMap<>();
|
||||
|
||||
public FrankBot(ResultSet set) throws SQLException {
|
||||
super(set);
|
||||
}
|
||||
|
||||
public FrankBot(Bot bot) {
|
||||
super(bot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlace(Habbo habbo, Room room) {
|
||||
super.onPlace(habbo, room);
|
||||
if (this.getRoomUnit() != null) {
|
||||
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
}
|
||||
|
||||
private static final short[] FRANK_OWNER_ACTIONS = { (short) Bot.ACTION_ROTATE };
|
||||
|
||||
@Override
|
||||
public short[] getOwnerActionIds() {
|
||||
return FRANK_OWNER_ACTIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostOwnerAction(int actionId) {
|
||||
if (actionId == ACTION_ROTATE && this.getRoomUnit() != null) {
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialise() {
|
||||
chatResponses.clear();
|
||||
doorLines = DEFAULT_DOOR_LINES;
|
||||
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
Statement statement = connection.createStatement();
|
||||
ResultSet set = statement.executeQuery("SELECT `keys`, `responses` FROM bot_chat_responses WHERE bot_type = '" + BOT_TYPE + "'")) {
|
||||
while (set.next()) {
|
||||
String keysRaw = set.getString("keys");
|
||||
String responsesRaw = set.getString("responses");
|
||||
|
||||
if (keysRaw == null || responsesRaw == null) continue;
|
||||
|
||||
List<String> responses = new ArrayList<>();
|
||||
for (String line : responsesRaw.split("\n")) {
|
||||
String trimmed = line.trim();
|
||||
if (!trimmed.isEmpty()) responses.add(trimmed);
|
||||
}
|
||||
|
||||
if (responses.isEmpty()) continue;
|
||||
|
||||
String firstKey = keysRaw.split(";", 2)[0].trim();
|
||||
if (firstKey.startsWith("__")) {
|
||||
switch (firstKey) {
|
||||
case KEY_DOOR_LINES:
|
||||
doorLines = new CopyOnWriteArrayList<>(responses);
|
||||
break;
|
||||
case KEY_BUSY_WHISPER:
|
||||
busyWhisper = responses.get(0);
|
||||
break;
|
||||
case KEY_DOOR_TRIGGERS:
|
||||
doorTriggerPattern = buildDoorTriggerPattern(responses);
|
||||
break;
|
||||
default:
|
||||
LOGGER.warn("FrankBot: unknown system key '{}', ignored", firstKey);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> shared = new CopyOnWriteArrayList<>(responses);
|
||||
|
||||
for (String key : keysRaw.split(";")) {
|
||||
if (chatResponses.size() >= MAX_CHAT_KEYWORDS) {
|
||||
LOGGER.warn("FrankBot: chat keyword cap ({}) reached, remaining rows ignored",
|
||||
MAX_CHAT_KEYWORDS);
|
||||
break;
|
||||
}
|
||||
String k = key == null ? "" : key.trim().toLowerCase();
|
||||
if (k.isEmpty()) continue;
|
||||
try {
|
||||
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(k) + "\\b");
|
||||
chatResponses.put(pattern, shared);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to compile Frank chat keyword pattern: {}", k, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.warn("FrankBot: could not load bot_chat_responses ({}). Frank will still serve items.", e.getMessage());
|
||||
}
|
||||
|
||||
ButlerBot.initialise();
|
||||
}
|
||||
|
||||
public static void dispose() {
|
||||
chatResponses.clear();
|
||||
doorLines = DEFAULT_DOOR_LINES;
|
||||
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
ButlerBot.dispose();
|
||||
}
|
||||
|
||||
private static Pattern buildDoorTriggerPattern(List<String> triggers) {
|
||||
StringBuilder sb = new StringBuilder("\\b(");
|
||||
boolean first = true;
|
||||
int count = 0;
|
||||
for (String trigger : triggers) {
|
||||
if (count >= MAX_DOOR_TRIGGERS) {
|
||||
LOGGER.warn("FrankBot: door trigger cap ({}) reached, extra entries ignored",
|
||||
MAX_DOOR_TRIGGERS);
|
||||
break;
|
||||
}
|
||||
String t = trigger == null ? "" : trigger.trim().toLowerCase();
|
||||
if (t.isEmpty()) continue;
|
||||
if (!first) sb.append('|');
|
||||
sb.append(Pattern.quote(t));
|
||||
first = false;
|
||||
count++;
|
||||
}
|
||||
sb.append(")\\b");
|
||||
|
||||
if (first) return DEFAULT_DOOR_PATTERN;
|
||||
|
||||
try {
|
||||
return Pattern.compile(sb.toString());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("FrankBot: failed to compile door trigger pattern from {}, falling back to default", triggers, e);
|
||||
return DEFAULT_DOOR_PATTERN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserSay(final RoomChatMessage message) {
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null) return;
|
||||
|
||||
Habbo asker = message.getHabbo();
|
||||
if (asker == null || asker.getClient() == null) return;
|
||||
|
||||
if (this.getRoomUnit() == null) return;
|
||||
|
||||
String raw = message.getUnfilteredMessage();
|
||||
if (raw != null && raw.length() > MAX_MESSAGE_LEN) return;
|
||||
|
||||
if (this.homeTile == null) {
|
||||
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
|
||||
if (this.busy.get() || this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw != null) {
|
||||
double distance = this.getRoomUnit().getCurrentLocation().distance(asker.getRoomUnit().getCurrentLocation());
|
||||
int commandDistance = Emulator.getConfig().getInt("hotel.bot.butler.commanddistance");
|
||||
|
||||
if (distance <= commandDistance) {
|
||||
String lower = raw.toLowerCase();
|
||||
|
||||
if (doorTriggerPattern.matcher(lower).find()) {
|
||||
if (!this.busy.compareAndSet(false, true)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
this.showToTheDoor(asker);
|
||||
return;
|
||||
}
|
||||
|
||||
for (java.util.Map.Entry<Pattern, List<String>> entry : chatResponses.entrySet()) {
|
||||
if (entry.getKey().matcher(lower).find()) {
|
||||
List<String> options = entry.getValue();
|
||||
if (options.isEmpty()) continue;
|
||||
|
||||
String reply = options.get(RANDOM.nextInt(options.size()));
|
||||
this.talk(reply);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.busy.compareAndSet(false, true)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
super.onUserSay(message);
|
||||
this.schedulePostServeReturn(currentRoom.getId(), 0);
|
||||
}
|
||||
|
||||
private void whisperThrottled(Habbo target, String text) {
|
||||
if (target == null || text == null || text.isEmpty() || this.getRoomUnit() == null) return;
|
||||
int userId = target.getHabboInfo().getId();
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastBusyWhisperAt.get(userId);
|
||||
if (last != null && (now - last) < BUSY_WHISPER_COOLDOWN_MS) return;
|
||||
lastBusyWhisperAt.put(userId, now);
|
||||
RoomChatMessage msg = new RoomChatMessage(text, this.getRoomUnit(), RoomChatMessageBubbles.BOT);
|
||||
target.getClient().sendResponse(new RoomUserWhisperComposer(msg));
|
||||
}
|
||||
|
||||
private void showToTheDoor(final Habbo target) {
|
||||
final Room room = this.getRoom();
|
||||
if (room == null || room.getLayout() == null || target == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final RoomTile doorTile = room.getLayout().getDoorTile();
|
||||
if (doorTile == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lookAt(target);
|
||||
List<String> lines = doorLines;
|
||||
String line = lines.isEmpty() ? DEFAULT_DOOR_LINES.get(RANDOM.nextInt(DEFAULT_DOOR_LINES.size()))
|
||||
: lines.get(RANDOM.nextInt(lines.size()));
|
||||
this.talk(line);
|
||||
|
||||
final int targetId = target.getHabboInfo().getId();
|
||||
final int roomId = room.getId();
|
||||
final AtomicBoolean fired = new AtomicBoolean(false);
|
||||
|
||||
final Runnable kickThenReturn = () -> {
|
||||
if (!fired.compareAndSet(false, true)) return;
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null || currentRoom.getId() != roomId) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
Habbo stillHere = currentRoom.getHabbo(targetId);
|
||||
if (stillHere != null) {
|
||||
currentRoom.kickHabbo(stillHere, false);
|
||||
}
|
||||
this.scheduleReturnHome(targetId, roomId, 0);
|
||||
};
|
||||
|
||||
if (this.getRoomUnit().canWalk() && !this.getRoomUnit().getCurrentLocation().equals(doorTile)) {
|
||||
List<Runnable> onArrive = new ArrayList<>();
|
||||
onArrive.add(kickThenReturn);
|
||||
|
||||
List<Runnable> onFail = new ArrayList<>();
|
||||
onFail.add(() -> Emulator.getThreading().run(kickThenReturn, 1500));
|
||||
|
||||
this.getRoomUnit().setGoalLocation(doorTile);
|
||||
Emulator.getThreading().run(
|
||||
new RoomUnitWalkToLocation(this.getRoomUnit(), doorTile, room, onArrive, onFail));
|
||||
} else {
|
||||
Emulator.getThreading().run(kickThenReturn, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int RETURN_HOME_POLL_MS = 500;
|
||||
private static final int RETURN_HOME_MAX_WAIT_MS = 8000;
|
||||
private static final int POST_SERVE_POLL_MS = 750;
|
||||
private static final int POST_SERVE_MAX_WAIT_MS = 30000;
|
||||
|
||||
private void schedulePostServeReturn(final int roomId, final int waitedMs) {
|
||||
if (waitedMs == 0 && !this.returnScheduled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (waitedMs >= POST_SERVE_MAX_WAIT_MS) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
if (this.homeTile == null) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(() -> {
|
||||
Room r = this.getRoom();
|
||||
if (r == null || r.getId() != roomId || this.getRoomUnit() == null || this.homeTile == null) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||
if (this.homeRotation != null && this.getRoomUnit().getBodyRotation() != this.homeRotation) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
this.persistPosition();
|
||||
} else {
|
||||
this.busy.set(false);
|
||||
}
|
||||
this.returnScheduled.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean stillWalking = this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)
|
||||
|| (this.getRoomUnit().getPath() != null && !this.getRoomUnit().getPath().isEmpty());
|
||||
|
||||
if (stillWalking) {
|
||||
this.schedulePostServeReturn(roomId, waitedMs + POST_SERVE_POLL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
this.returnScheduled.set(false);
|
||||
this.returnHome(-1, false);
|
||||
}, POST_SERVE_POLL_MS);
|
||||
}
|
||||
|
||||
private void scheduleReturnHome(final int kickedHabboId, final int roomId, final int waitedMs) {
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null || currentRoom.getId() != roomId) return;
|
||||
|
||||
boolean stillEscorting = currentRoom.getHabbo(kickedHabboId) != null;
|
||||
|
||||
if (!stillEscorting || waitedMs >= RETURN_HOME_MAX_WAIT_MS) {
|
||||
this.returnHome(kickedHabboId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(
|
||||
() -> this.scheduleReturnHome(kickedHabboId, roomId, waitedMs + RETURN_HOME_POLL_MS),
|
||||
RETURN_HOME_POLL_MS);
|
||||
}
|
||||
|
||||
private void returnHome(int kickedHabboId, boolean alwaysTeleport) {
|
||||
final Room room = this.getRoom();
|
||||
if (room == null || this.homeTile == null || this.getRoomUnit() == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final Runnable teleportHome = () -> {
|
||||
Room r = this.getRoom();
|
||||
if (r == null || this.getRoomUnit() == null) return;
|
||||
|
||||
double homeZ = r.getTopHeightAt(this.homeTile.x, this.homeTile.y);
|
||||
|
||||
this.getRoomUnit().stopWalking();
|
||||
this.getRoomUnit().setZ(homeZ);
|
||||
this.getRoomUnit().setLocation(this.homeTile);
|
||||
this.getRoomUnit().setPreviousLocationZ(homeZ);
|
||||
if (this.homeRotation != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
}
|
||||
this.getRoomUnit().statusUpdate(true);
|
||||
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
this.persistPosition();
|
||||
};
|
||||
|
||||
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||
if (this.homeRotation != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
room.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
}
|
||||
this.persistPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasOtherWatchers = false;
|
||||
for (Habbo h : room.getCurrentHabbos().values()) {
|
||||
if (h.getHabboInfo().getId() != kickedHabboId) {
|
||||
hasOtherWatchers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alwaysTeleport || !hasOtherWatchers || !this.getRoomUnit().canWalk()) {
|
||||
teleportHome.run();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Runnable> onArrive = new ArrayList<>();
|
||||
onArrive.add(() -> {
|
||||
if (this.homeRotation != null && this.getRoom() != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
this.getRoom().sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
}
|
||||
this.persistPosition();
|
||||
});
|
||||
|
||||
List<Runnable> onFail = new ArrayList<>();
|
||||
onFail.add(teleportHome);
|
||||
|
||||
this.getRoomUnit().setGoalLocation(this.homeTile);
|
||||
Emulator.getThreading().run(
|
||||
new RoomUnitWalkToLocation(this.getRoomUnit(), this.homeTile, room, onArrive, onFail));
|
||||
}
|
||||
|
||||
private void persistPosition() {
|
||||
this.needsUpdate(true);
|
||||
this.run();
|
||||
this.busy.set(false);
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,8 @@ public class CatalogManager {
|
||||
public final Item ecotronItem;
|
||||
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
|
||||
private final List<Voucher> vouchers;
|
||||
public final TIntObjectMap<int[]> furnitureValues;
|
||||
private volatile byte[] rareValuesPayloadCache;
|
||||
|
||||
public CatalogManager() {
|
||||
long millis = System.currentTimeMillis();
|
||||
@@ -219,6 +221,7 @@ public class CatalogManager {
|
||||
this.buildersClubOfferDefs = new TIntIntHashMap();
|
||||
this.vouchers = new ArrayList<>();
|
||||
this.limitedNumbers = new THashMap<>();
|
||||
this.furnitureValues = new TIntObjectHashMap<>();
|
||||
|
||||
this.initialize();
|
||||
|
||||
@@ -243,6 +246,76 @@ public class CatalogManager {
|
||||
this.loadClothing();
|
||||
this.loadRecycler();
|
||||
this.loadGiftWrappers();
|
||||
this.loadFurnitureValues();
|
||||
}
|
||||
|
||||
private synchronized void loadFurnitureValues() {
|
||||
this.furnitureValues.clear();
|
||||
final int diamondType = Emulator.getConfig().getInt("seasonal.currency.diamond", 5);
|
||||
|
||||
for (CatalogPage page : this.catalogPages.valueCollection()) {
|
||||
for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) {
|
||||
if (catalogItem.getAmount() != 1)
|
||||
continue;
|
||||
|
||||
int credits = catalogItem.getCredits();
|
||||
int points = catalogItem.getPoints();
|
||||
int pointsType = catalogItem.getPointsType();
|
||||
|
||||
if (points <= 0 || pointsType != diamondType)
|
||||
continue;
|
||||
|
||||
THashSet<Item> baseItems = catalogItem.getBaseItems();
|
||||
|
||||
if (baseItems.size() != 1)
|
||||
continue;
|
||||
|
||||
for (Item item : baseItems) {
|
||||
FurnitureType type = item.getType();
|
||||
|
||||
if (type != FurnitureType.FLOOR && type != FurnitureType.WALL)
|
||||
continue;
|
||||
|
||||
int spriteId = item.getSpriteId();
|
||||
|
||||
if (spriteId > 0 && !this.furnitureValues.containsKey(spriteId)) {
|
||||
this.furnitureValues.put(spriteId, new int[]{credits, points, pointsType});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.rebuildRareValuesPayloadCache();
|
||||
|
||||
LOGGER.info("Furniture Values -> Loaded! ({} entries)", this.furnitureValues.size());
|
||||
}
|
||||
|
||||
private void rebuildRareValuesPayloadCache() {
|
||||
try (java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(this.furnitureValues.size() * 16 + 8);
|
||||
java.io.DataOutputStream out = new java.io.DataOutputStream(baos)) {
|
||||
out.writeInt(this.furnitureValues.size());
|
||||
TIntObjectIterator<int[]> iterator = this.furnitureValues.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.advance();
|
||||
int[] value = iterator.value();
|
||||
out.writeInt(iterator.key()); // spriteId
|
||||
out.writeInt(value[0]); // credits
|
||||
out.writeInt(value[1]); // points
|
||||
out.writeInt(value[2]); // pointsType
|
||||
}
|
||||
this.rareValuesPayloadCache = baos.toByteArray();
|
||||
} catch (java.io.IOException e) {
|
||||
LOGGER.error("Failed to build rare values payload cache", e);
|
||||
this.rareValuesPayloadCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
public TIntObjectMap<int[]> getFurnitureValues() {
|
||||
return this.furnitureValues;
|
||||
}
|
||||
|
||||
public byte[] getRareValuesPayloadSnapshot() {
|
||||
return this.rareValuesPayloadCache;
|
||||
}
|
||||
|
||||
private synchronized void loadLimitedNumbers() {
|
||||
@@ -1046,10 +1119,19 @@ public class CatalogManager {
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
|
||||
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
|
||||
String baseName = baseItem.getName();
|
||||
String type = item.getName().replace("rentable_bot_", "");
|
||||
type = type.replace("bot_", "");
|
||||
type = type.replace("visitor_logger", "visitor_log");
|
||||
|
||||
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|
||||
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
|
||||
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
|
||||
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
THashMap<String, String> data = new THashMap<>();
|
||||
|
||||
for (String s : item.getExtradata().split(";")) {
|
||||
|
||||
+7
-3
@@ -42,14 +42,18 @@ public class RoomBundleLayout extends SingleBundle {
|
||||
}
|
||||
|
||||
if (this.room == null) {
|
||||
if (this.roomId > 0) {
|
||||
this.room = Emulator.getGameEnvironment().getRoomManager().loadRoom(this.roomId);
|
||||
RoomManager roomManager = Emulator.getGameEnvironment().getRoomManager();
|
||||
if (this.roomId > 0 && roomManager != null) {
|
||||
this.room = roomManager.loadRoom(this.roomId);
|
||||
|
||||
if (this.room != null)
|
||||
this.room.preventUnloading = true;
|
||||
} else {
|
||||
} else if (this.roomId <= 0) {
|
||||
LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId());
|
||||
}
|
||||
// roomManager can be null when CatalogManager.loadFurnitureValues() runs
|
||||
// during GameEnvironment.load() before RoomManager is constructed; in that
|
||||
// case skip eager room loading — the bundle resolves lazily at runtime.
|
||||
}
|
||||
|
||||
if (this.room == null) {
|
||||
|
||||
@@ -301,7 +301,6 @@ public class CommandHandler {
|
||||
addCommand(new GivePrefixCommand());
|
||||
addCommand(new ListPrefixesCommand());
|
||||
addCommand(new RemovePrefixCommand());
|
||||
addCommand(new PrefixBlacklistCommand());
|
||||
addCommand(new WiredCommand());
|
||||
addCommand(new TestCommand());
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.modtool.WordFilter;
|
||||
import com.eu.habbo.habbohotel.modtool.WordFilterWord;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -21,30 +22,44 @@ public class FilterWordCommand extends Command {
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
if (params.length < 2) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"));
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String word = params[1];
|
||||
|
||||
// Optional trailing "prefix" keyword marks the word as prefix-only (blocks
|
||||
// custom prefixes but not chat/motto/guild). Usage:
|
||||
// :filterword <word> -> everywhere, default replacement
|
||||
// :filterword <word> <replacement> -> everywhere
|
||||
// :filterword <word> prefix -> prefix-only, default replacement
|
||||
// :filterword <word> <replacement> prefix -> prefix-only
|
||||
boolean prefixOnly = false;
|
||||
String replacement = WordFilter.DEFAULT_REPLACEMENT;
|
||||
if (params.length == 3) {
|
||||
replacement = params[2];
|
||||
|
||||
if (params.length >= 3) {
|
||||
if (params[params.length - 1].equalsIgnoreCase("prefix")) {
|
||||
prefixOnly = true;
|
||||
if (params.length >= 4) replacement = params[2];
|
||||
} else {
|
||||
replacement = params[2];
|
||||
}
|
||||
}
|
||||
|
||||
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement);
|
||||
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement, prefixOnly);
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`) VALUES (?, ?)")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`, `prefix_only`) VALUES (?, ?, ?)")) {
|
||||
statement.setString(1, word);
|
||||
statement.setString(2, replacement);
|
||||
statement.setString(3, prefixOnly ? "1" : "0");
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"));
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement));
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement) + (prefixOnly ? " [prefix-only]" : ""), RoomChatMessageBubbles.ALERT);
|
||||
Emulator.getGameEnvironment().getWordFilter().addWord(wordFilterWord);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class PrefixBlacklistCommand extends Command {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PrefixBlacklistCommand.class);
|
||||
|
||||
public PrefixBlacklistCommand() {
|
||||
super("cmd_prefix_blacklist", Emulator.getTexts().getValue("commands.keys.cmd_prefix_blacklist").split(";"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
if (params.length < 2) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String action = params[1].toLowerCase();
|
||||
|
||||
if (action.equals("list")) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.header")).append("\r");
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist ORDER BY word")) {
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
int count = 0;
|
||||
while (set.next()) {
|
||||
sb.append("- ").append(set.getString("word")).append("\r");
|
||||
count++;
|
||||
}
|
||||
if (count == 0) {
|
||||
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.empty"));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error listing prefix blacklist", e);
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (params.length < 3) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String word = params[2].toLowerCase().trim();
|
||||
|
||||
if (word.isEmpty()) {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.empty_word"), RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action.equals("add")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO custom_prefix_blacklist (word) VALUES (?)")) {
|
||||
statement.setString(1, word);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error adding prefix blacklist word", e);
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.added").replace("%word%", word),
|
||||
RoomChatMessageBubbles.ALERT
|
||||
);
|
||||
} else if (action.equals("remove")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM custom_prefix_blacklist WHERE word = ?")) {
|
||||
statement.setString(1, word);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error removing prefix blacklist word", e);
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.removed").replace("%word%", word),
|
||||
RoomChatMessageBubbles.ALERT
|
||||
);
|
||||
} else {
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,13 @@ public class GameClient {
|
||||
this.channel.close();
|
||||
|
||||
if (this.habbo != null) {
|
||||
if (this.habbo.isOnline()) {
|
||||
// Agisci sull'Habbo SOLO se è ancora attaccato a QUESTO client. Su un
|
||||
// reconnect veloce (drop Cloudflare → il client riconnette) l'Habbo può
|
||||
// essere già stato riassegnato alla NUOVA connessione (session resume):
|
||||
// in quel caso questo dispose della vecchia connessione NON deve
|
||||
// parcheggiarlo né disconnetterlo, altrimenti ucciderebbe la sessione
|
||||
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
|
||||
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
|
||||
// Try to park the habbo in the grace period instead of immediate disconnect
|
||||
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
|
||||
|
||||
|
||||
+20
-4
@@ -118,16 +118,32 @@ public class SessionResumeManager {
|
||||
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
|
||||
}
|
||||
|
||||
clearSsoTicket(habbo.getHabboInfo().getId());
|
||||
// NON svuotare il ticket SSO qui. Dietro Cloudflare la pagina si ricarica
|
||||
// lentamente (~15s) e la grace (5s) scade prima che la nuova connessione
|
||||
// arrivi: svuotando il ticket si cancellava quello NUOVO appena scritto dal
|
||||
// CMS per il refresh → "non-existing SSO token" → bisognava refreshare 2 volte.
|
||||
// Il ticket vive col suo TTL (auth_ticket_expires_at) e viene sovrascritto dal
|
||||
// CMS al prossimo /client o azzerato al logout.
|
||||
}
|
||||
|
||||
private void restoreSsoTicket(int userId, String ssoTicket) {
|
||||
// Restore the old ticket ONLY if no fresh ticket has been written in the
|
||||
// meantime. On a hard-refresh the CMS writes a NEW auth_ticket for the same
|
||||
// user before this parking restore runs; without the guard we'd clobber it
|
||||
// with the old ticket, so the new connection's SSO wouldn't be found and the
|
||||
// client would get "session expired" on the first attempt. The guard means:
|
||||
// normal reconnect (ticket cleared to '' after login) -> restore; hard-refresh
|
||||
// (CMS already wrote a new ticket) -> leave the new ticket untouched.
|
||||
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
|
||||
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? AND (auth_ticket = '' OR auth_ticket IS NULL) LIMIT 1")) {
|
||||
statement.setString(1, ssoTicket);
|
||||
statement.setInt(2, userId);
|
||||
statement.execute();
|
||||
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
|
||||
int updated = statement.executeUpdate();
|
||||
if (updated > 0) {
|
||||
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
|
||||
} else {
|
||||
LOGGER.info("[SessionResume] Skipped SSO restore for user {} — a newer ticket is already present (likely a fresh login/hard-refresh)", userId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
|
||||
}
|
||||
|
||||
@@ -252,6 +252,25 @@ public class Guild implements Runnable {
|
||||
return this.readForum;
|
||||
}
|
||||
|
||||
public boolean canHabboReadForum(int habboId, GuildMember member, boolean staff) {
|
||||
if (staff || this.getOwnerId() == habboId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (this.readForum) {
|
||||
case EVERYONE:
|
||||
return true;
|
||||
case MEMBERS:
|
||||
return member != null && member.getRank().type <= GuildRank.MEMBER.type;
|
||||
case ADMINS:
|
||||
return member != null && member.getRank().type < GuildRank.MEMBER.type;
|
||||
case OWNER:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setReadForum(SettingsState readForum) {
|
||||
this.readForum = readForum;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ public class Item implements ISerialize {
|
||||
return item.getName().toLowerCase().startsWith("a0 pet");
|
||||
}
|
||||
|
||||
public static boolean isBot(Item item) {
|
||||
if (item == null) return false;
|
||||
String name = item.getName();
|
||||
return name != null && (name.startsWith("bot_") || name.startsWith("rentable_bot_"));
|
||||
}
|
||||
|
||||
public static double getCurrentHeight(HabboItem item) {
|
||||
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
|
||||
if (item.getExtradata().isEmpty()) {
|
||||
|
||||
+4
@@ -29,6 +29,10 @@ public class InteractionRoomAds extends InteractionCustomValues {
|
||||
{
|
||||
this.put("offsetZ", "0");
|
||||
}
|
||||
|
||||
{
|
||||
this.put("scale", "100");
|
||||
}
|
||||
};
|
||||
|
||||
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.eu.habbo.habbohotel.mentions;
|
||||
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HabboMention {
|
||||
|
||||
public static final int TYPE_DIRECT = 0;
|
||||
public static final int TYPE_ROOM = 1;
|
||||
|
||||
private final int id;
|
||||
private final int targetUserId;
|
||||
private final int senderUserId;
|
||||
private final String senderUsername;
|
||||
private final int roomId;
|
||||
private final String roomName;
|
||||
private final String message;
|
||||
private final int mentionType;
|
||||
private final int timestamp;
|
||||
private final boolean read;
|
||||
|
||||
public HabboMention(ResultSet set) throws SQLException {
|
||||
this.id = set.getInt("id");
|
||||
this.targetUserId = set.getInt("target_user_id");
|
||||
this.senderUserId = set.getInt("sender_user_id");
|
||||
this.senderUsername = set.getString("sender_username");
|
||||
this.roomId = set.getInt("room_id");
|
||||
this.roomName = set.getString("room_name");
|
||||
this.message = set.getString("message");
|
||||
this.mentionType = set.getInt("mention_type");
|
||||
this.timestamp = set.getInt("timestamp");
|
||||
this.read = set.getInt("read") == 1;
|
||||
}
|
||||
|
||||
public HabboMention(int targetUserId, int id, Habbo sender, Room room, String roomName, String message, int mentionType, int timestamp) {
|
||||
this.id = id;
|
||||
this.targetUserId = targetUserId;
|
||||
this.senderUserId = sender.getHabboInfo().getId();
|
||||
this.senderUsername = sender.getHabboInfo().getUsername();
|
||||
this.roomId = room.getId();
|
||||
this.roomName = roomName;
|
||||
this.message = message;
|
||||
this.mentionType = mentionType;
|
||||
this.timestamp = timestamp;
|
||||
this.read = false;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public int getTargetUserId() {
|
||||
return this.targetUserId;
|
||||
}
|
||||
|
||||
public int getSenderUserId() {
|
||||
return this.senderUserId;
|
||||
}
|
||||
|
||||
public String getSenderUsername() {
|
||||
return this.senderUsername;
|
||||
}
|
||||
|
||||
public int getRoomId() {
|
||||
return this.roomId;
|
||||
}
|
||||
|
||||
public String getRoomName() {
|
||||
return this.roomName;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public int getMentionType() {
|
||||
return this.mentionType;
|
||||
}
|
||||
|
||||
public int getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public boolean isRead() {
|
||||
return this.read;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package com.eu.habbo.habbohotel.mentions;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.messenger.MessengerBuddy;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatType;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class MentionManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class);
|
||||
|
||||
private static final int ROOM_NAME_MAX_LENGTH = 64;
|
||||
private static final int MESSAGE_MAX_LENGTH = 255;
|
||||
|
||||
private final ConcurrentHashMap<Integer, Long> cooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> roomBroadcastCooldowns = new ConcurrentHashMap<>();
|
||||
|
||||
// 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<>();
|
||||
private final ConcurrentHashMap<Integer, Long> deleteCooldowns = new ConcurrentHashMap<>();
|
||||
|
||||
private volatile long lastPrune = System.currentTimeMillis();
|
||||
private static final long PRUNE_INTERVAL_MS = 5 * 60_000L;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
Set<String> aliases = new HashSet<>();
|
||||
String raw = Emulator.getConfig().getValue(configKey, defaultValue);
|
||||
for (String alias : raw.split(",")) {
|
||||
String trimmed = alias.trim().toLowerCase();
|
||||
if (!trimmed.isEmpty()) {
|
||||
aliases.add(trimmed);
|
||||
}
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private Set<String> roomAliases() {
|
||||
return parseAliases("mentions.room.aliases", "room,stanza");
|
||||
}
|
||||
|
||||
private Set<String> friendsAliases() {
|
||||
return parseAliases("mentions.friends.aliases", "friends,amici");
|
||||
}
|
||||
|
||||
private Set<String> everyoneAliases() {
|
||||
return parseAliases("mentions.everyone.aliases", "all,everyone,tutti");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
Set<String> room) {
|
||||
if (alias.isEmpty()) return BroadcastScope.NONE;
|
||||
if (everyone.contains(alias)) return BroadcastScope.EVERYONE;
|
||||
if (friends.contains(alias)) return BroadcastScope.FRIENDS;
|
||||
if (room.contains(alias)) return BroadcastScope.ROOM;
|
||||
return BroadcastScope.NONE;
|
||||
}
|
||||
|
||||
public void process(Habbo sender, Room room, String message, RoomChatType type) {
|
||||
try {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender == null || room == null || message == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.isEmpty() || message.indexOf('@') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int senderId = sender.getHabboInfo().getId();
|
||||
long now = System.currentTimeMillis();
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
|
||||
Long last = this.cooldowns.get(senderId);
|
||||
if (last != null && (now - last) < cooldownMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> roomAliases = this.roomAliases();
|
||||
Set<String> friendsAliases = this.friendsAliases();
|
||||
Set<String> everyoneAliases = this.everyoneAliases();
|
||||
|
||||
BroadcastScope broadcastScope = BroadcastScope.NONE;
|
||||
LinkedHashSet<String> directTokens = new LinkedHashSet<>();
|
||||
|
||||
for (String token : message.split("\\s+")) {
|
||||
if (token.length() < 2 || token.charAt(0) != '@') {
|
||||
continue;
|
||||
}
|
||||
|
||||
String raw = token.substring(1);
|
||||
String aliasCandidate = trimTrailingPunctuation(raw).toLowerCase();
|
||||
|
||||
BroadcastScope scope = this.classifyAlias(aliasCandidate, everyoneAliases, friendsAliases, roomAliases);
|
||||
|
||||
if (scope != BroadcastScope.NONE) {
|
||||
// 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;
|
||||
}
|
||||
} else if (!raw.isEmpty()) {
|
||||
directTokens.add(raw);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
broadcastScope = BroadcastScope.NONE;
|
||||
}
|
||||
|
||||
if (broadcastScope == BroadcastScope.NONE && directTokens.isEmpty()) {
|
||||
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);
|
||||
if (lastRoom != null && (now - lastRoom) < roomCooldownMs) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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<>();
|
||||
Set<Integer> seen = new HashSet<>();
|
||||
|
||||
switch (broadcastScope) {
|
||||
case EVERYONE:
|
||||
this.collectEveryoneTargets(senderId, targets, seen, maxTargets);
|
||||
break;
|
||||
case FRIENDS:
|
||||
this.collectFriendsTargets(sender, senderId, targets, seen, maxTargets);
|
||||
break;
|
||||
case ROOM:
|
||||
this.collectRoomTargets(room, senderId, targets, seen, maxTargets);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
int processed = 0;
|
||||
for (String token : directTokens) {
|
||||
if (processed++ >= maxDirectTokens) break;
|
||||
Habbo habbo = this.resolveHabbo(room, token);
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
|
||||
continue;
|
||||
}
|
||||
if (seen.add(habbo.getHabboInfo().getId())) {
|
||||
targets.add(habbo);
|
||||
}
|
||||
if (targets.size() >= maxTargets) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cooldowns.put(senderId, now);
|
||||
if (broadcastScope != BroadcastScope.NONE) this.roomBroadcastCooldowns.put(senderId, now);
|
||||
this.pruneCooldownsIfDue(now);
|
||||
|
||||
int mentionType = (broadcastScope != BroadcastScope.NONE) ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT;
|
||||
int timestamp = Emulator.getIntUnixTimestamp();
|
||||
String roomName = truncate(room.getName(), ROOM_NAME_MAX_LENGTH);
|
||||
String storedMessage = truncate(message, MESSAGE_MAX_LENGTH);
|
||||
|
||||
for (Habbo target : targets) {
|
||||
this.store(target, sender, room, storedMessage, mentionType, timestamp, roomName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to process mentions.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectRoomTargets(Room room, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
|
||||
for (Habbo habbo : room.getHabbos()) {
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
|
||||
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
|
||||
if (targets.size() >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
|
||||
private void collectFriendsTargets(Habbo sender, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
|
||||
if (sender.getMessenger() == null) return;
|
||||
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
|
||||
for (MessengerBuddy buddy : sender.getMessenger().getFriends().values()) {
|
||||
if (buddy == null) continue;
|
||||
int buddyId = buddy.getId();
|
||||
if (buddyId == senderId) continue;
|
||||
Habbo online = habboManager.getHabbo(buddyId);
|
||||
if (online == null) continue;
|
||||
if (seen.add(buddyId)) targets.add(online);
|
||||
if (targets.size() >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
|
||||
private void collectEveryoneTargets(int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
|
||||
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
|
||||
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
|
||||
if (targets.size() >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
|
||||
private void store(Habbo target, Habbo sender, Room room, String message, int mentionType, int timestamp, String roomName) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO habbo_mentions (target_user_id, sender_user_id, sender_username, room_id, room_name, message, mention_type, timestamp, `read`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
statement.setInt(1, target.getHabboInfo().getId());
|
||||
statement.setInt(2, sender.getHabboInfo().getId());
|
||||
statement.setString(3, sender.getHabboInfo().getUsername());
|
||||
statement.setInt(4, room.getId());
|
||||
statement.setString(5, roomName);
|
||||
statement.setString(6, message);
|
||||
statement.setInt(7, mentionType);
|
||||
statement.setInt(8, timestamp);
|
||||
statement.executeUpdate();
|
||||
|
||||
int generatedId = 0;
|
||||
try (ResultSet keys = statement.getGeneratedKeys()) {
|
||||
if (keys.next()) {
|
||||
generatedId = keys.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
HabboMention mention = new HabboMention(target.getHabboInfo().getId(), generatedId, sender, room, roomName, message, mentionType, timestamp);
|
||||
|
||||
if (target.getClient() != null) {
|
||||
target.getClient().sendResponse(new com.eu.habbo.messages.outgoing.mentions.MentionReceivedComposer(mention));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store mention.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<HabboMention> getMentions(int userId, int limit) {
|
||||
List<HabboMention> mentions = new ArrayList<>();
|
||||
if (limit <= 0) limit = 50;
|
||||
if (limit > 200) limit = 200;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM habbo_mentions WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, limit);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
mentions.add(new HabboMention(set));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to load mentions.", e);
|
||||
}
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public void markRead(int userId, int mode, int mentionId) {
|
||||
// Caller has already validated mode and mentionId; this method is defensive only.
|
||||
if (mode != 0 && mode != 1) return;
|
||||
if (mode == 1 && mentionId <= 0) return;
|
||||
|
||||
String query = mode == 1
|
||||
? "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND id = ? AND `read` = 0"
|
||||
: "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND `read` = 0";
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(query)) {
|
||||
statement.setInt(1, userId);
|
||||
if (mode == 1) {
|
||||
statement.setInt(2, mentionId);
|
||||
}
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to mark mentions as read.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(int userId, int mentionId) {
|
||||
if (mentionId <= 0) return;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"DELETE FROM habbo_mentions WHERE target_user_id = ? AND id = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, mentionId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete mention.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
if (mode == 1) {
|
||||
cooldownMs = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
|
||||
bucket = this.markReadCooldowns;
|
||||
} else {
|
||||
cooldownMs = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
|
||||
bucket = this.markAllCooldowns;
|
||||
}
|
||||
return tryAcquire(bucket, userId, cooldownMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
private boolean tryAcquire(ConcurrentHashMap<Integer, Long> bucket, int userId, long cooldownMs) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = bucket.get(userId);
|
||||
if (last != null && (now - last) < cooldownMs) {
|
||||
return false;
|
||||
}
|
||||
bucket.put(userId, now);
|
||||
this.pruneCooldownsIfDue(now);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
long mentionWindow = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
|
||||
long roomWindow = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
|
||||
long requestWindow = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
|
||||
long markReadWindow = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
|
||||
long markAllWindow = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
|
||||
long deleteWindow = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
|
||||
|
||||
prune(this.cooldowns, now, mentionWindow);
|
||||
prune(this.roomBroadcastCooldowns, now, roomWindow);
|
||||
prune(this.requestListCooldowns, now, requestWindow);
|
||||
prune(this.markReadCooldowns, now, markReadWindow);
|
||||
prune(this.markAllCooldowns, now, markAllWindow);
|
||||
prune(this.deleteCooldowns, now, deleteWindow);
|
||||
}
|
||||
|
||||
private static void prune(ConcurrentHashMap<Integer, Long> bucket, long now, long windowMs) {
|
||||
Iterator<Map.Entry<Integer, Long>> it = bucket.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<Integer, Long> entry = it.next();
|
||||
Long value = entry.getValue();
|
||||
if (value == null || (now - value) >= windowMs) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TRAILING_PUNCTUATION = ".,!?;:)]}\"'";
|
||||
|
||||
private static String trimTrailingPunctuation(String value) {
|
||||
int end = value.length();
|
||||
while (end > 0 && TRAILING_PUNCTUATION.indexOf(value.charAt(end - 1)) >= 0) {
|
||||
end--;
|
||||
}
|
||||
return value.substring(0, end);
|
||||
}
|
||||
|
||||
private static String truncate(String value, int max) {
|
||||
if (value == null) return "";
|
||||
if (value.length() <= max) return value;
|
||||
return value.substring(0, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Habbo resolveHabbo(Room room, String rawToken) {
|
||||
Habbo habbo = room.getHabbo(rawToken);
|
||||
if (habbo != null) {
|
||||
return habbo;
|
||||
}
|
||||
String trimmed = trimTrailingPunctuation(rawToken);
|
||||
if (!trimmed.isEmpty() && !trimmed.equals(rawToken)) {
|
||||
return room.getHabbo(trimmed);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ public class WordFilter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WordFilter.class);
|
||||
|
||||
private static final Pattern DIACRITICS_AND_FRIENDS = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
|
||||
//Configuration. Loaded from database & updated accordingly.
|
||||
public static boolean ENABLED_FRIENDCHAT = true;
|
||||
public static String DEFAULT_REPLACEMENT = "bobba";
|
||||
protected THashSet<WordFilterWord> autoReportWords = new THashSet<>();
|
||||
@@ -63,10 +62,12 @@ public class WordFilter {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (word.autoReport)
|
||||
this.autoReportWords.add(word);
|
||||
else if (word.hideMessage)
|
||||
this.hideMessageWords.add(word);
|
||||
if (!word.prefixOnly) {
|
||||
if (word.autoReport)
|
||||
this.autoReportWords.add(word);
|
||||
else if (word.hideMessage)
|
||||
this.hideMessageWords.add(word);
|
||||
}
|
||||
|
||||
this.words.add(word);
|
||||
}
|
||||
@@ -146,6 +147,8 @@ public class WordFilter {
|
||||
while (iterator.hasNext()) {
|
||||
WordFilterWord word = (WordFilterWord) iterator.next();
|
||||
|
||||
if (word.prefixOnly) continue;
|
||||
|
||||
if (StringUtils.containsIgnoreCase(filteredMessage, word.key)) {
|
||||
if (habbo != null) {
|
||||
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
|
||||
@@ -179,6 +182,8 @@ public class WordFilter {
|
||||
while (iterator.hasNext()) {
|
||||
WordFilterWord word = (WordFilterWord) iterator.next();
|
||||
|
||||
if (word.prefixOnly) continue;
|
||||
|
||||
if (StringUtils.containsIgnoreCase(message, word.key)) {
|
||||
if (habbo != null) {
|
||||
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
|
||||
|
||||
@@ -9,6 +9,7 @@ public class WordFilterWord {
|
||||
public final boolean hideMessage;
|
||||
public final boolean autoReport;
|
||||
public final int muteTime;
|
||||
public final boolean prefixOnly;
|
||||
|
||||
public WordFilterWord(ResultSet set) throws SQLException {
|
||||
this.key = set.getString("key");
|
||||
@@ -16,13 +17,27 @@ public class WordFilterWord {
|
||||
this.hideMessage = set.getInt("hide") == 1;
|
||||
this.autoReport = set.getInt("report") == 1;
|
||||
this.muteTime = set.getInt("mute");
|
||||
this.prefixOnly = readBooleanColumn(set, "prefix_only");
|
||||
}
|
||||
|
||||
public WordFilterWord(String key, String replacement) {
|
||||
this(key, replacement, false);
|
||||
}
|
||||
|
||||
public WordFilterWord(String key, String replacement, boolean prefixOnly) {
|
||||
this.key = key;
|
||||
this.replacement = replacement;
|
||||
this.hideMessage = false;
|
||||
this.autoReport = false;
|
||||
this.muteTime = 0;
|
||||
this.prefixOnly = prefixOnly;
|
||||
}
|
||||
|
||||
private static boolean readBooleanColumn(ResultSet set, String column) {
|
||||
try {
|
||||
return set.getInt(column) == 1;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public class Permission {
|
||||
public static String ACC_SEE_WHISPERS = "acc_see_whispers";
|
||||
public static String ACC_SEE_TENTCHAT = "acc_see_tentchat";
|
||||
public static String ACC_SUPERWIRED = "acc_superwired";
|
||||
public static String ACC_HOUSEKEEPING = "acc_housekeeping";
|
||||
public static String ACC_SUPPORTTOOL = "acc_supporttool";
|
||||
public static String ACC_UNKICKABLE = "acc_unkickable";
|
||||
public static String ACC_GUILDGATE = "acc_guildgate";
|
||||
|
||||
@@ -197,6 +197,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK;
|
||||
private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK;
|
||||
private boolean youtubeEnabled = false;
|
||||
private boolean soundboardEnabled = false;
|
||||
private String youtubeCurrentVideo = "";
|
||||
private String youtubeSenderName = "";
|
||||
private final java.util.List<String> youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
@@ -204,6 +205,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
|
||||
public boolean isYoutubeEnabled() { return this.youtubeEnabled; }
|
||||
public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; }
|
||||
public boolean isSoundboardEnabled() { return this.soundboardEnabled; }
|
||||
public void setSoundboardEnabled(boolean enabled) { this.soundboardEnabled = enabled; }
|
||||
public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; }
|
||||
public String getYoutubeSenderName() { return this.youtubeSenderName; }
|
||||
public java.util.List<String> getYoutubePlaylist() { return this.youtubePlaylist; }
|
||||
@@ -250,6 +253,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.allowWalkthrough = set.getBoolean("allow_walkthrough");
|
||||
this.hideWall = set.getBoolean("allow_hidewall");
|
||||
try { this.youtubeEnabled = set.getBoolean("youtube_enabled"); } catch (Exception e) { this.youtubeEnabled = false; }
|
||||
try { this.soundboardEnabled = set.getBoolean("soundboard_enabled"); } catch (Exception e) { this.soundboardEnabled = false; }
|
||||
this.chatMode = set.getInt("chat_mode");
|
||||
this.chatWeight = set.getInt("chat_weight");
|
||||
this.chatSpeed = set.getInt("chat_speed");
|
||||
|
||||
@@ -134,7 +134,18 @@ public class RoomChatMessageBubbles {
|
||||
}
|
||||
|
||||
public static RoomChatMessageBubbles getBubble(int id) {
|
||||
return BUBBLES.getOrDefault(id, NORMAL);
|
||||
RoomChatMessageBubbles bubble = BUBBLES.get(id);
|
||||
if (bubble != null) return bubble;
|
||||
|
||||
// Custom chat bubbles (client-side only, e.g. ids 253+) are not registered
|
||||
// above. Instead of falling back to NORMAL (which made them render as the
|
||||
// default bubble), pass the id through so the server relays it as-is and
|
||||
// the client renders its own .bubble-<id> style. Capped to avoid abuse.
|
||||
if (id > 0 && id <= 1000) {
|
||||
return new RoomChatMessageBubbles(id, "CUSTOM_" + id, "", true, false);
|
||||
}
|
||||
|
||||
return NORMAL;
|
||||
}
|
||||
|
||||
private static void registerBubble(RoomChatMessageBubbles bubble) {
|
||||
|
||||
@@ -1020,6 +1020,10 @@ public class RoomManager {
|
||||
room.getYoutubeWatchers()).compose());
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer(
|
||||
room.isSoundboardEnabled(),
|
||||
Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose());
|
||||
|
||||
WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit());
|
||||
room.habboEntered(habbo);
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.eu.habbo.habbohotel.soundboard;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SoundboardManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SoundboardManager.class);
|
||||
|
||||
private final List<SoundboardSound> sounds = new ArrayList<>();
|
||||
|
||||
public SoundboardManager() {
|
||||
long millis = System.currentTimeMillis();
|
||||
this.bootstrap();
|
||||
this.reload();
|
||||
LOGGER.info("Soundboard Manager -> Loaded! ({} MS, {} sounds)", System.currentTimeMillis() - millis, this.sounds.size());
|
||||
}
|
||||
|
||||
// Self-bootstrap: room flag column + sounds table, so the feature works even
|
||||
// before the manual migration is applied.
|
||||
private void bootstrap() {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute("ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0");
|
||||
statement.execute("CREATE TABLE IF NOT EXISTS `soundboard_sounds` (" +
|
||||
"`id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL DEFAULT '', " +
|
||||
"`url` VARCHAR(255) NOT NULL DEFAULT '', `enabled` TINYINT(1) NOT NULL DEFAULT 1, " +
|
||||
"`sort_order` INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to bootstrap soundboard schema", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
this.sounds.clear();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT id, name, url FROM soundboard_sounds WHERE enabled = 1 ORDER BY sort_order ASC, id ASC");
|
||||
ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
this.sounds.add(new SoundboardSound(set));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to load soundboard sounds", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<SoundboardSound> getSounds() {
|
||||
return this.sounds;
|
||||
}
|
||||
|
||||
public SoundboardSound getSound(int id) {
|
||||
for (SoundboardSound sound : this.sounds) {
|
||||
if (sound.id == id) return sound;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Owner toggle — persists the room flag with a dedicated UPDATE (kept out of
|
||||
// the big room-settings save to avoid touching that statement).
|
||||
public void setRoomEnabled(int roomId, boolean enabled) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET soundboard_enabled = ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setString(1, enabled ? "1" : "0");
|
||||
statement.setInt(2, roomId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to set soundboard_enabled for room {}", roomId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.eu.habbo.habbohotel.soundboard;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
// One soundboard pad: a named audio clip served from a URL (uploaded via the CMS).
|
||||
public class SoundboardSound {
|
||||
public final int id;
|
||||
public final String name;
|
||||
public final String url;
|
||||
|
||||
public SoundboardSound(ResultSet set) throws SQLException {
|
||||
this.id = set.getInt("id");
|
||||
this.name = set.getString("name");
|
||||
this.url = set.getString("url");
|
||||
}
|
||||
}
|
||||
@@ -132,15 +132,12 @@ public class HabboManager {
|
||||
Emulator.getPluginManager().fireEvent(new UserRegisteredEvent(habbo));
|
||||
}
|
||||
|
||||
if (!Emulator.debugging) {
|
||||
try (PreparedStatement stmt = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
|
||||
stmt.setString(1, "");
|
||||
stmt.setInt(2, habbo.getHabboInfo().getId());
|
||||
stmt.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
}
|
||||
// NB: il ticket SSO NON viene svuotato qui di proposito. Dietro
|
||||
// Cloudflare il WebSocket viene droppato e il client ritenta più
|
||||
// volte con lo STESSO ticket: se lo consumassimo al primo uso, i
|
||||
// retry (e l'hard-refresh) fallirebbero con "non-existing SSO token".
|
||||
// Il ticket resta valido fino alla scadenza (auth_ticket_expires_at,
|
||||
// TTL gestito dal CMS) o finché il CMS non ne scrive uno nuovo / logout.
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
|
||||
@@ -89,6 +89,7 @@ public class HabboStats implements Runnable {
|
||||
public long lastTradeTimestamp = Emulator.getIntUnixTimestamp();
|
||||
public long lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
||||
public long lastPurchaseTimestamp = Emulator.getIntUnixTimestamp();
|
||||
public long lastFloorplanSaveTimestamp = 0;
|
||||
public int uiFlags;
|
||||
public boolean hasGottenDefaultSavedSearches;
|
||||
private HabboInfo habboInfo;
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
package com.eu.habbo.habbohotel.wheel;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.items.Item;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboItem;
|
||||
import gnu.trove.set.hash.THashSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public class WheelManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WheelManager.class);
|
||||
private static final int RECENT_KEEP = 50;
|
||||
private static final int SECONDS_PER_DAY = 86400;
|
||||
|
||||
public static final Set<String> VALID_PRIZE_TYPES = Set.of(
|
||||
"credits", "points", "spin", "item", "badge", "nothing");
|
||||
public static final int MAX_PRIZES_PER_SAVE = 64;
|
||||
public static final int MAX_STRING_LEN = 64;
|
||||
public static final int MAX_PRIZE_AMOUNT = 1_000_000;
|
||||
public static final int MAX_ITEM_QUANTITY = 100;
|
||||
public static final int MAX_WEIGHT = 1_000_000;
|
||||
public static final int MAX_EXTRA_SPINS = 10_000;
|
||||
private static final long MIN_SPIN_INTERVAL_MS = 1500L;
|
||||
|
||||
private final List<WheelPrize> prizes = new ArrayList<>();
|
||||
private int totalWeight = 0;
|
||||
private int freeSpinsPerDay = 1;
|
||||
private int spinCost = 50;
|
||||
private int spinCostType = 5;
|
||||
|
||||
private final ConcurrentHashMap<Integer, Long> lastSpinAt = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, WheelUserState> userStateCache = new ConcurrentHashMap<>();
|
||||
private final java.util.concurrent.CopyOnWriteArrayList<WheelRecentWin> recentWinsCache = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
|
||||
public WheelManager() {
|
||||
long millis = System.currentTimeMillis();
|
||||
this.reload();
|
||||
LOGGER.info("Wheel Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
this.loadSettings();
|
||||
this.loadPrizes();
|
||||
this.loadRecentWins();
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
this.freeSpinsPerDay = Emulator.getConfig().getInt("wheel.free_spins_per_day", 1);
|
||||
this.spinCost = Emulator.getConfig().getInt("wheel.spin_cost", 50);
|
||||
this.spinCostType = Emulator.getConfig().getInt("wheel.spin_cost_type", 5);
|
||||
}
|
||||
|
||||
private void loadPrizes() {
|
||||
this.prizes.clear();
|
||||
this.totalWeight = 0;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT * FROM wheel_prizes WHERE enabled = 1 ORDER BY sort_order ASC, id ASC");
|
||||
ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
WheelPrize prize = new WheelPrize(set);
|
||||
this.prizes.add(prize);
|
||||
this.totalWeight += prize.weight;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to load fortune wheel prizes", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<WheelPrize> getPrizes() {
|
||||
return this.prizes;
|
||||
}
|
||||
|
||||
public int getSpinCost() {
|
||||
return this.spinCost;
|
||||
}
|
||||
|
||||
public int getSpinCostType() {
|
||||
return this.spinCostType;
|
||||
}
|
||||
|
||||
private int today() {
|
||||
return Emulator.getIntUnixTimestamp() / SECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
public synchronized WheelUserState getUserState(int userId) {
|
||||
int today = this.today();
|
||||
WheelUserState cached = this.userStateCache.get(userId);
|
||||
|
||||
if (cached != null) {
|
||||
if (cached.lastReset != today) {
|
||||
cached.freeSpins = this.freeSpinsPerDay;
|
||||
cached.lastReset = today;
|
||||
this.persistUserState(userId, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
WheelUserState state = new WheelUserState();
|
||||
boolean exists = false;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT free_spins, extra_spins, last_reset FROM wheel_user_state WHERE user_id = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) {
|
||||
state.freeSpins = set.getInt("free_spins");
|
||||
state.extraSpins = set.getInt("extra_spins");
|
||||
state.lastReset = set.getInt("last_reset");
|
||||
exists = true;
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to read wheel state for user {}", userId, e);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
state.freeSpins = this.freeSpinsPerDay;
|
||||
state.extraSpins = 0;
|
||||
state.lastReset = today;
|
||||
this.persistUserState(userId, state);
|
||||
} else if (state.lastReset != today) {
|
||||
state.freeSpins = this.freeSpinsPerDay;
|
||||
state.lastReset = today;
|
||||
this.persistUserState(userId, state);
|
||||
}
|
||||
|
||||
this.userStateCache.put(userId, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
private void persistUserState(int userId, WheelUserState state) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO wheel_user_state (user_id, free_spins, extra_spins, last_reset) VALUES (?, ?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE free_spins = VALUES(free_spins), extra_spins = VALUES(extra_spins), last_reset = VALUES(last_reset)")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, state.freeSpins);
|
||||
statement.setInt(3, state.extraSpins);
|
||||
statement.setInt(4, state.lastReset);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to persist wheel state for user {}", userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized WheelPrize spin(Habbo habbo) {
|
||||
int userId = habbo.getHabboInfo().getId();
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = this.lastSpinAt.get(userId);
|
||||
if (last != null && (now - last) < MIN_SPIN_INTERVAL_MS) return null;
|
||||
this.lastSpinAt.put(userId, now);
|
||||
|
||||
WheelUserState state = this.getUserState(userId);
|
||||
|
||||
boolean usedFree;
|
||||
if (state.freeSpins > 0) {
|
||||
state.freeSpins--;
|
||||
usedFree = true;
|
||||
} else if (state.extraSpins > 0) {
|
||||
state.extraSpins--;
|
||||
usedFree = false;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
WheelPrize prize = this.pickWeighted();
|
||||
if (prize == null) {
|
||||
if (usedFree) state.freeSpins++; else state.extraSpins++;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.giveReward(habbo, prize, state);
|
||||
this.persistUserState(userId, state);
|
||||
this.recordWin(habbo, prize);
|
||||
|
||||
return prize;
|
||||
}
|
||||
|
||||
private WheelPrize pickWeighted() {
|
||||
if (this.prizes.isEmpty() || this.totalWeight <= 0) return null;
|
||||
|
||||
int roll = ThreadLocalRandom.current().nextInt(this.totalWeight);
|
||||
int acc = 0;
|
||||
for (WheelPrize prize : this.prizes) {
|
||||
acc += prize.weight;
|
||||
if (roll < acc) return prize;
|
||||
}
|
||||
return this.prizes.get(this.prizes.size() - 1);
|
||||
}
|
||||
|
||||
private void giveReward(Habbo habbo, WheelPrize prize, WheelUserState state) {
|
||||
int amount = Math.max(0, Math.min(prize.amount, MAX_PRIZE_AMOUNT));
|
||||
|
||||
switch (prize.type) {
|
||||
case "credits":
|
||||
if (amount > 0) habbo.giveCredits(amount);
|
||||
break;
|
||||
case "points":
|
||||
if (amount > 0) habbo.givePoints(prize.pointsType, amount);
|
||||
break;
|
||||
case "spin":
|
||||
int room = Math.max(0, MAX_EXTRA_SPINS - state.extraSpins);
|
||||
state.extraSpins += Math.min(amount, room);
|
||||
break;
|
||||
case "item":
|
||||
this.giveItem(habbo, prize, Math.min(amount, MAX_ITEM_QUANTITY));
|
||||
break;
|
||||
case "badge":
|
||||
if (prize.value != null && !prize.value.isEmpty()) {
|
||||
habbo.addBadge(prize.value, "Fortune Wheel");
|
||||
}
|
||||
break;
|
||||
case "nothing":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void giveItem(Habbo habbo, WheelPrize prize, int quantity) {
|
||||
if (quantity <= 0 || prize.value == null) return;
|
||||
|
||||
int baseId;
|
||||
try {
|
||||
baseId = Integer.parseInt(prize.value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
Item base = Emulator.getGameEnvironment().getItemManager().getItem(baseId);
|
||||
if (base == null) return;
|
||||
|
||||
THashSet<HabboItem> items = new THashSet<>();
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), base, 0, 0, "");
|
||||
if (item != null) items.add(item);
|
||||
}
|
||||
|
||||
if (!items.isEmpty()) {
|
||||
habbo.addFurniture(items);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordWin(Habbo habbo, WheelPrize prize) {
|
||||
WheelRecentWin win = new WheelRecentWin(
|
||||
habbo.getHabboInfo().getUsername(),
|
||||
habbo.getHabboInfo().getLook(),
|
||||
prize.label);
|
||||
|
||||
this.recentWinsCache.add(0, win);
|
||||
while (this.recentWinsCache.size() > RECENT_KEEP) {
|
||||
this.recentWinsCache.remove(this.recentWinsCache.size() - 1);
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO wheel_recent_wins (user_id, username, look, prize_label, won_at) VALUES (?, ?, ?, ?, ?)")) {
|
||||
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||
statement.setString(2, habbo.getHabboInfo().getUsername());
|
||||
statement.setString(3, habbo.getHabboInfo().getLook());
|
||||
statement.setString(4, prize.label);
|
||||
statement.setInt(5, Emulator.getIntUnixTimestamp());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
|
||||
try (PreparedStatement trim = connection.prepareStatement(
|
||||
"DELETE FROM wheel_recent_wins WHERE id < (SELECT id FROM (SELECT id FROM wheel_recent_wins ORDER BY id DESC LIMIT 1 OFFSET ?) t)")) {
|
||||
trim.setInt(1, RECENT_KEEP - 1);
|
||||
trim.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to record wheel win", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<WheelRecentWin> getRecentWins(int limit) {
|
||||
if (limit <= 0) return new ArrayList<>();
|
||||
int size = this.recentWinsCache.size();
|
||||
if (size == 0) return new ArrayList<>();
|
||||
int take = Math.min(limit, size);
|
||||
return new ArrayList<>(this.recentWinsCache.subList(0, take));
|
||||
}
|
||||
|
||||
private void loadRecentWins() {
|
||||
this.recentWinsCache.clear();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT username, look, prize_label FROM wheel_recent_wins ORDER BY id DESC LIMIT ?")) {
|
||||
statement.setInt(1, RECENT_KEEP);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
this.recentWinsCache.add(new WheelRecentWin(
|
||||
set.getString("username"),
|
||||
set.getString("look"),
|
||||
set.getString("prize_label")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to load wheel recent wins", e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean buySpin(Habbo habbo) {
|
||||
if (this.spinCost <= 0) return false;
|
||||
|
||||
int userId = habbo.getHabboInfo().getId();
|
||||
WheelUserState state = this.getUserState(userId);
|
||||
if (state.extraSpins >= MAX_EXTRA_SPINS) return false;
|
||||
|
||||
if (this.spinCostType == -1) {
|
||||
if (habbo.getHabboInfo().getCredits() < this.spinCost) return false;
|
||||
habbo.giveCredits(-this.spinCost);
|
||||
} else {
|
||||
if (habbo.getHabboInfo().getCurrencyAmount(this.spinCostType) < this.spinCost) return false;
|
||||
habbo.givePoints(this.spinCostType, -this.spinCost);
|
||||
}
|
||||
|
||||
state.extraSpins++;
|
||||
this.persistUserState(userId, state);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a single prize. An {@code id <= 0} inserts a brand-new prize and
|
||||
* returns its generated id; a positive id updates the existing row (and
|
||||
* re-enables it, so a previously soft-deleted prize can be brought back).
|
||||
* {@code sortOrder} reflects the prize's position in the editor so the
|
||||
* wheel layout matches what the admin sees. Returns the effective row id,
|
||||
* or {@code 0} if the write failed.
|
||||
*/
|
||||
public int savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label, int sortOrder) {
|
||||
String safeType = (type != null && VALID_PRIZE_TYPES.contains(type)) ? type : "nothing";
|
||||
String safeValue = truncate(value, MAX_STRING_LEN);
|
||||
String safeLabel = truncate(label, MAX_STRING_LEN);
|
||||
int safeAmount = clamp(amount, 0, MAX_PRIZE_AMOUNT);
|
||||
int safeWeight = clamp(weight, 0, MAX_WEIGHT);
|
||||
int safeSort = clamp(sortOrder, 0, MAX_PRIZES_PER_SAVE);
|
||||
|
||||
if (id > 0) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ?, sort_order = ?, enabled = 1 WHERE id = ?")) {
|
||||
statement.setString(1, safeType);
|
||||
statement.setString(2, safeValue);
|
||||
statement.setInt(3, safeAmount);
|
||||
statement.setInt(4, pointsType);
|
||||
statement.setInt(5, safeWeight);
|
||||
statement.setString(6, safeLabel);
|
||||
statement.setInt(7, safeSort);
|
||||
statement.setInt(8, id);
|
||||
statement.executeUpdate();
|
||||
return id;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to save wheel prize {}", id, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO wheel_prizes (type, value, amount, points_type, weight, label, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, 1, ?)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
statement.setString(1, safeType);
|
||||
statement.setString(2, safeValue);
|
||||
statement.setInt(3, safeAmount);
|
||||
statement.setInt(4, pointsType);
|
||||
statement.setInt(5, safeWeight);
|
||||
statement.setString(6, safeLabel);
|
||||
statement.setInt(7, safeSort);
|
||||
statement.executeUpdate();
|
||||
|
||||
try (ResultSet keys = statement.getGeneratedKeys()) {
|
||||
if (keys.next()) return keys.getInt(1);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to insert wheel prize", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-deletes every enabled prize whose id is not in {@code keptIds} by
|
||||
* setting {@code enabled = 0}. This is intentionally non-destructive: rows
|
||||
* stay in the table (so historical references and re-enabling remain
|
||||
* possible) but {@link #loadPrizes()} only ever loads {@code enabled = 1}.
|
||||
* An empty set disables all prizes.
|
||||
*/
|
||||
public void disablePrizesNotIn(Set<Integer> keptIds) {
|
||||
if (keptIds == null) return;
|
||||
|
||||
StringBuilder sql = new StringBuilder("UPDATE wheel_prizes SET enabled = 0 WHERE enabled = 1");
|
||||
if (!keptIds.isEmpty()) {
|
||||
StringJoiner ids = new StringJoiner(",", " AND id NOT IN (", ")");
|
||||
for (Integer keptId : keptIds) {
|
||||
ids.add(Integer.toString(keptId));
|
||||
}
|
||||
sql.append(ids);
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to disable removed wheel prizes", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncate(String s, int max) {
|
||||
if (s == null) return "";
|
||||
return s.length() <= max ? s : s.substring(0, max);
|
||||
}
|
||||
|
||||
private static int clamp(int value, int min, int max) {
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.eu.habbo.habbohotel.wheel;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.items.Item;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
// One slice of the wheel. type = item | badge | credits | points | spin | nothing.
|
||||
public class WheelPrize {
|
||||
public final int id;
|
||||
public final String type;
|
||||
public final String value; // item: base item id ; badge: badge code ; others: unused
|
||||
public final int amount; // item qty / credits / points / extra spins
|
||||
public final int pointsType; // for type=points
|
||||
public final int weight;
|
||||
public final String label;
|
||||
public final int spriteId; // resolved for item prizes so the client can render the furni icon
|
||||
|
||||
public WheelPrize(ResultSet set) throws SQLException {
|
||||
this.id = set.getInt("id");
|
||||
this.type = set.getString("type");
|
||||
this.value = set.getString("value");
|
||||
this.amount = set.getInt("amount");
|
||||
this.pointsType = set.getInt("points_type");
|
||||
this.weight = Math.max(0, set.getInt("weight"));
|
||||
this.label = set.getString("label");
|
||||
this.spriteId = resolveSpriteId(this.type, this.value);
|
||||
}
|
||||
|
||||
private static int resolveSpriteId(String type, String value) {
|
||||
if (!"item".equals(type) || value == null) return 0;
|
||||
try {
|
||||
Item item = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(value.trim()));
|
||||
return item != null ? item.getSpriteId() : 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String badgeCode() {
|
||||
return "badge".equals(this.type) && this.value != null ? this.value : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.eu.habbo.habbohotel.wheel;
|
||||
|
||||
// A row in the "latest winners" panel. Denormalized (username/look stored at win time).
|
||||
public class WheelRecentWin {
|
||||
public final String username;
|
||||
public final String look;
|
||||
public final String prizeLabel;
|
||||
|
||||
public WheelRecentWin(String username, String look, String prizeLabel) {
|
||||
this.username = username != null ? username : "";
|
||||
this.look = look != null ? look : "";
|
||||
this.prizeLabel = prizeLabel != null ? prizeLabel : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.eu.habbo.habbohotel.wheel;
|
||||
|
||||
// Per-user spin balance. freeSpins resets daily (lazy, on access); extraSpins persist.
|
||||
public class WheelUserState {
|
||||
public int freeSpins;
|
||||
public int extraSpins;
|
||||
public int lastReset; // day index (unix / 86400) of the last daily reset
|
||||
|
||||
public int totalSpins() {
|
||||
return this.freeSpins + this.extraSpins;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import com.eu.habbo.messages.incoming.hotelview.*;
|
||||
import com.eu.habbo.messages.incoming.inventory.*;
|
||||
import com.eu.habbo.messages.incoming.inventory.nickicons.*;
|
||||
import com.eu.habbo.messages.incoming.inventory.prefixes.*;
|
||||
import com.eu.habbo.messages.incoming.mentions.*;
|
||||
import com.eu.habbo.messages.incoming.modtool.*;
|
||||
import com.eu.habbo.messages.incoming.navigator.*;
|
||||
import com.eu.habbo.messages.incoming.polls.AnswerPollEvent;
|
||||
@@ -426,6 +427,9 @@ public class PacketManager {
|
||||
}
|
||||
|
||||
void registerRooms() throws Exception {
|
||||
this.registerHandler(Incoming.RequestMentionsEvent, RequestMentionsEvent.class);
|
||||
this.registerHandler(Incoming.MarkMentionsReadEvent, MarkMentionsReadEvent.class);
|
||||
this.registerHandler(Incoming.DeleteMentionEvent, DeleteMentionEvent.class);
|
||||
this.registerHandler(Incoming.RequestRoomLoadEvent, RequestRoomLoadEvent.class);
|
||||
this.registerHandler(Incoming.RequestHeightmapEvent, RequestRoomHeightmapEvent.class);
|
||||
this.registerHandler(Incoming.RequestRoomHeightmapEvent, RequestRoomHeightmapEvent.class);
|
||||
@@ -719,5 +723,42 @@ public class PacketManager {
|
||||
this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class);
|
||||
this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class);
|
||||
this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.class);
|
||||
|
||||
// Housekeeping (in-client admin panel)
|
||||
this.registerHandler(Incoming.HousekeepingFindUserByNameEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByNameEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingFindUserByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByIdEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingBanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingBanUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingUnbanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingUnbanUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingMuteUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingKickUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingForceDisconnectUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingForceDisconnectUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSetUserRankEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetUserRankEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingTradeLockUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTradeLockUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingResetUserPasswordEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingResetUserPasswordEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingFindRoomByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindRoomByIdEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSearchRoomsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSearchRoomsEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingRoomStateEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingRoomStateEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingMuteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteRoomEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingKickAllFromRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickAllFromRoomEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingTransferRoomOwnershipEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTransferRoomOwnershipEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingDeleteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingDeleteRoomEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGiveCreditsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCreditsEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGiveCurrencyEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCurrencyEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGrantItemEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGrantItemEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSetHcSubscriptionEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetHcSubscriptionEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSendHotelAlertEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSendHotelAlertEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGetDashboardEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGetDashboardEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingListActionLogEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingListActionLogEvent.class);
|
||||
|
||||
this.registerHandler(Incoming.RequestRareValuesEvent, com.eu.habbo.messages.incoming.rarevalues.RequestRareValuesEvent.class);
|
||||
|
||||
this.registerHandler(Incoming.WheelOpenEvent, com.eu.habbo.messages.incoming.wheel.WheelOpenEvent.class);
|
||||
this.registerHandler(Incoming.WheelSpinEvent, com.eu.habbo.messages.incoming.wheel.WheelSpinEvent.class);
|
||||
this.registerHandler(Incoming.WheelBuySpinEvent, com.eu.habbo.messages.incoming.wheel.WheelBuySpinEvent.class);
|
||||
this.registerHandler(Incoming.WheelAdminGetPrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminGetPrizesEvent.class);
|
||||
this.registerHandler(Incoming.WheelAdminSavePrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminSavePrizesEvent.class);
|
||||
|
||||
this.registerHandler(Incoming.SoundboardPlayEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardPlayEvent.class);
|
||||
this.registerHandler(Incoming.SoundboardSetEnabledEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardSetEnabledEvent.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,4 +460,43 @@ public class Incoming {
|
||||
public static final int YouTubeRoomPlayEvent = 8001;
|
||||
public static final int YouTubeRoomWatchingEvent = 8002;
|
||||
public static final int YouTubeRoomSettingsEvent = 8003;
|
||||
|
||||
// Housekeeping (in-client admin panel) — IDs 9100..9199 reserved
|
||||
public static final int HousekeepingFindUserByNameEvent = 9100;
|
||||
public static final int HousekeepingFindUserByIdEvent = 9101;
|
||||
public static final int HousekeepingBanUserEvent = 9102;
|
||||
public static final int HousekeepingUnbanUserEvent = 9103;
|
||||
public static final int HousekeepingMuteUserEvent = 9104;
|
||||
public static final int HousekeepingKickUserEvent = 9105;
|
||||
public static final int HousekeepingForceDisconnectUserEvent = 9106;
|
||||
public static final int HousekeepingSetUserRankEvent = 9107;
|
||||
public static final int HousekeepingTradeLockUserEvent = 9108;
|
||||
public static final int HousekeepingResetUserPasswordEvent = 9109;
|
||||
public static final int HousekeepingFindRoomByIdEvent = 9110;
|
||||
public static final int HousekeepingSearchRoomsEvent = 9111;
|
||||
public static final int HousekeepingRoomStateEvent = 9112;
|
||||
public static final int HousekeepingMuteRoomEvent = 9113;
|
||||
public static final int HousekeepingKickAllFromRoomEvent = 9114;
|
||||
public static final int HousekeepingTransferRoomOwnershipEvent = 9115;
|
||||
public static final int HousekeepingDeleteRoomEvent = 9116;
|
||||
public static final int HousekeepingGiveCreditsEvent = 9117;
|
||||
public static final int HousekeepingGiveCurrencyEvent = 9118;
|
||||
public static final int HousekeepingGrantItemEvent = 9119;
|
||||
public static final int HousekeepingSetHcSubscriptionEvent = 9120;
|
||||
public static final int HousekeepingSendHotelAlertEvent = 9121;
|
||||
public static final int HousekeepingGetDashboardEvent = 9122;
|
||||
public static final int HousekeepingListActionLogEvent = 9123;
|
||||
|
||||
// Custom features — IDs 9300+ reserved
|
||||
public static final int RequestRareValuesEvent = 9300;
|
||||
public static final int WheelOpenEvent = 9301;
|
||||
public static final int WheelSpinEvent = 9302;
|
||||
public static final int WheelBuySpinEvent = 9303;
|
||||
public static final int WheelAdminGetPrizesEvent = 9304;
|
||||
public static final int WheelAdminSavePrizesEvent = 9305;
|
||||
public static final int SoundboardPlayEvent = 9306;
|
||||
public static final int SoundboardSetEnabledEvent = 9307;
|
||||
public static final int RequestMentionsEvent = 4803;
|
||||
public static final int MarkMentionsReadEvent = 4804;
|
||||
public static final int DeleteMentionEvent = 4805;
|
||||
}
|
||||
|
||||
+13
@@ -175,6 +175,19 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
CatalogItem item = page.getCatalogItem(itemId);
|
||||
|
||||
// Search-results gift sends the catalog offer_id as
|
||||
// itemId, not catalog_items.id - see the same fix in
|
||||
// CatalogBuyItemEvent. Fall back to scanning the
|
||||
// page for the matching offer_id.
|
||||
if (item == null) {
|
||||
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
|
||||
if (candidate != null && candidate.getOfferId() == itemId) {
|
||||
item = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
LOGGER.debug("catalog item null -> {}", itemId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
|
||||
+41
-7
@@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.bots.BotManager;
|
||||
import com.eu.habbo.habbohotel.catalog.*;
|
||||
import com.eu.habbo.habbohotel.catalog.layouts.*;
|
||||
import com.eu.habbo.habbohotel.items.FurnitureType;
|
||||
import com.eu.habbo.habbohotel.items.Item;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.pets.PetManager;
|
||||
import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport;
|
||||
@@ -201,15 +202,48 @@ public class CatalogBuyItemEvent extends MessageHandler {
|
||||
|
||||
else
|
||||
item = page.getCatalogItem(itemId);
|
||||
// temp patch, can a dev with better knowledge than me look into this asap pls.
|
||||
if (page instanceof BotsLayout) {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS) && this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||
return;
|
||||
|
||||
// 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) {
|
||||
item = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (page instanceof PetsLayout) {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS) && this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
|
||||
// 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;
|
||||
|
||||
if (item != null) {
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (baseItem == null) continue;
|
||||
if (Item.isBot(baseItem)) itemHasBot = true;
|
||||
if (Item.isPet(baseItem)) itemHasPet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemHasBot && !this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS)
|
||||
&& this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemHasPet) {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS)
|
||||
&& this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + ""));
|
||||
return;
|
||||
}
|
||||
|
||||
+227
-123
@@ -4,23 +4,36 @@ import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.*;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboItem;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer;
|
||||
import gnu.trove.set.hash.THashSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FloorPlanEditorSaveEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FloorPlanEditorSaveEvent.class);
|
||||
|
||||
public static int MAXIMUM_FLOORPLAN_WIDTH_LENGTH = 64;
|
||||
public static int MAXIMUM_FLOORPLAN_SIZE = 64 * 64;
|
||||
|
||||
private static final int SAVE_COOLDOWN_SECONDS = 3;
|
||||
private static final int MAX_AUTO_PICKUP_ITEMS = 500;
|
||||
private static final Pattern ALLOWED_MAP_CHARS = Pattern.compile("[a-zA-Z0-9\r]+");
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
@@ -38,153 +51,244 @@ public class FloorPlanEditorSaveEvent extends MessageHandler {
|
||||
if (room == null)
|
||||
return;
|
||||
|
||||
if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
|
||||
StringJoiner errors = new StringJoiner("<br />");
|
||||
String map = this.packet.readString();
|
||||
map = map.replace("X", "x");
|
||||
if (!(room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER))) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] mapRows = map.split("\r");
|
||||
long now = Emulator.getIntUnixTimestamp();
|
||||
if (now - this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp < SAVE_COOLDOWN_SECONDS) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, "Please wait a few seconds before saving again."));
|
||||
return;
|
||||
}
|
||||
|
||||
int firstRowSize = mapRows[0].length();
|
||||
StringJoiner errors = new StringJoiner("<br />");
|
||||
String map = this.packet.readString();
|
||||
|
||||
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
|
||||
if (!map.matches("[a-zA-Z0-9\r]+")) errors.add("${notification.floorplan_editor.error.title}");
|
||||
if (map == null || map.length() > MAXIMUM_FLOORPLAN_SIZE) {
|
||||
LOGGER.warn("Floorplan save rejected (oversize): user={} room={} mapLen={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map == null ? 0 : map.length());
|
||||
return;
|
||||
}
|
||||
|
||||
Arrays.stream(mapRows)
|
||||
.filter(line -> line.length() != firstRowSize)
|
||||
.findAny()
|
||||
.ifPresent(s -> errors.add("(General): Line " + (Arrays.asList(mapRows).indexOf(s) + 1) + " is of different length than line 1"));
|
||||
if (!ALLOWED_MAP_CHARS.matcher(map).matches()) {
|
||||
LOGGER.warn("Floorplan save rejected (illegal chars): user={} room={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (map.isEmpty() || map.replace("x", "").replace("\r", "").isEmpty()) {
|
||||
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
|
||||
map = map.replace("X", "x");
|
||||
|
||||
String[] mapRows = map.split("\r");
|
||||
|
||||
if (mapRows.length == 0 || mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
int firstRowSize = mapRows[0].length();
|
||||
|
||||
if (firstRowSize == 0 || firstRowSize > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String row : mapRows) {
|
||||
if (row.length() != firstRowSize) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
|
||||
if (map.replace("x", "").replace("\r", "").isEmpty()) {
|
||||
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
|
||||
}
|
||||
}
|
||||
|
||||
int doorX = this.packet.readInt();
|
||||
int doorY = this.packet.readInt();
|
||||
|
||||
if (doorX < 0 || doorX >= firstRowSize || doorY < 0 || doorY >= mapRows.length) {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
|
||||
} else if (mapRows[doorY].charAt(doorX) == 'x') {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
|
||||
}
|
||||
|
||||
int doorRotation = this.packet.readInt();
|
||||
if (doorRotation < 0 || doorRotation > 7) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
|
||||
}
|
||||
|
||||
int wallSize = this.packet.readInt();
|
||||
if (wallSize < -2 || wallSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
|
||||
}
|
||||
int floorSize = this.packet.readInt();
|
||||
if (floorSize < -2 || floorSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
|
||||
}
|
||||
|
||||
int wallHeight = -1;
|
||||
if (this.packet.bytesAvailable() >= 4)
|
||||
wallHeight = this.packet.readInt();
|
||||
|
||||
if (wallHeight < -1 || wallHeight > 15) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
|
||||
}
|
||||
|
||||
boolean autoPickup = false;
|
||||
if (this.packet.bytesAvailable() >= 1) {
|
||||
autoPickup = this.packet.readBoolean();
|
||||
}
|
||||
|
||||
if (errors.length() > 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
|
||||
THashSet<RoomTile> new_tileList = new THashSet<>();
|
||||
THashSet<HabboItem> itemsToPickup = new THashSet<>();
|
||||
int blockedX = -1;
|
||||
int blockedY = -1;
|
||||
blockingRoomItemScan:
|
||||
for (int y = 0; y < mapRows.length; y++) {
|
||||
for (int x = 0; x < firstRowSize; x++) {
|
||||
|
||||
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
|
||||
new_tileList.add(tile);
|
||||
String square = String.valueOf(mapRows[y].charAt(x));
|
||||
short height;
|
||||
|
||||
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
|
||||
if (autoPickup) {
|
||||
THashSet<HabboItem> here = room.getItemsAt(x, y);
|
||||
if (here != null) itemsToPickup.addAll(here);
|
||||
continue;
|
||||
}
|
||||
blockedX = x;
|
||||
blockedY = y;
|
||||
break blockingRoomItemScan;
|
||||
}
|
||||
|
||||
if (map.length() > MAXIMUM_FLOORPLAN_SIZE) {
|
||||
errors.add("${notification.floorplan_editor.error.message.too_large_area}");
|
||||
}
|
||||
|
||||
if (mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) errors.add("${notification.floorplan_editor.error.message.too_large_height}");
|
||||
else if (Arrays.stream(mapRows).anyMatch(l -> l.length() > MAXIMUM_FLOORPLAN_WIDTH_LENGTH || l.isEmpty())) errors.add("${notification.floorplan_editor.error.message.too_large_width}");
|
||||
|
||||
if (errors.length() > 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
|
||||
try {
|
||||
if (square.isEmpty()) {
|
||||
height = 0;
|
||||
} else if (Emulator.isNumeric(square)) {
|
||||
height = Short.parseShort(square);
|
||||
} else {
|
||||
int idx = "abcdefghijklmnopqrstuvwxyz".indexOf(square.toLowerCase());
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
height = (short) Math.min(26, 10 + idx);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int doorX = this.packet.readInt();
|
||||
int doorY = this.packet.readInt();
|
||||
|
||||
if (doorX < 0 || doorX > firstRowSize || doorY < 0 || doorY >= mapRows.length) {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
|
||||
}
|
||||
|
||||
if (doorY < mapRows.length && doorX < mapRows[doorY].length() && mapRows[doorY].charAt(doorX) == 'x') {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
|
||||
}
|
||||
|
||||
int doorRotation = this.packet.readInt();
|
||||
if (doorRotation < 0 || doorRotation > 7) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
|
||||
}
|
||||
|
||||
int wallSize = this.packet.readInt();
|
||||
if (wallSize < -2 || wallSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
|
||||
}
|
||||
int floorSize = this.packet.readInt();
|
||||
if (floorSize < -2 || floorSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
|
||||
}
|
||||
|
||||
int wallHeight = -1;
|
||||
if (this.packet.bytesAvailable() >= 4)
|
||||
wallHeight = this.packet.readInt();
|
||||
|
||||
if (wallHeight < -1 || wallHeight > 15) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
|
||||
}
|
||||
|
||||
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
|
||||
THashSet<RoomTile> new_tileList = new THashSet<>();
|
||||
blockingRoomItemScan:
|
||||
for (int y = 0; y < mapRows.length; y++) {
|
||||
for (int x = 0; x < firstRowSize; x++) {
|
||||
|
||||
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
|
||||
new_tileList.add(tile);
|
||||
String square = String.valueOf(mapRows[y].charAt(x));
|
||||
short height;
|
||||
|
||||
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
|
||||
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
|
||||
break blockingRoomItemScan;
|
||||
} else {
|
||||
if (square.isEmpty()) {
|
||||
height = 0;
|
||||
} else if (Emulator.isNumeric(square)) {
|
||||
height = Short.parseShort(square);
|
||||
} else {
|
||||
height = (short) (10 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(square.toUpperCase()));
|
||||
}
|
||||
}
|
||||
|
||||
if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
|
||||
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
|
||||
break blockingRoomItemScan;
|
||||
if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
|
||||
if (autoPickup) {
|
||||
THashSet<HabboItem> here = room.getItemsAt(x, y);
|
||||
if (here != null) itemsToPickup.addAll(here);
|
||||
continue;
|
||||
}
|
||||
blockedX = x;
|
||||
blockedY = y;
|
||||
break blockingRoomItemScan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockedX < 0) {
|
||||
locked_tileList.removeAll(new_tileList);
|
||||
if (!locked_tileList.isEmpty()) {
|
||||
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
|
||||
if (autoPickup) {
|
||||
for (RoomTile lt : locked_tileList) {
|
||||
THashSet<HabboItem> here = room.getItemsAt(lt.x, lt.y);
|
||||
if (here != null) itemsToPickup.addAll(here);
|
||||
}
|
||||
} else {
|
||||
RoomTile first = locked_tileList.iterator().next();
|
||||
blockedX = first.x;
|
||||
blockedY = first.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockedX >= 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
|
||||
"${notification.floorplan_editor.error.message.change_blocked_by_room_item} (" + blockedX + ", " + blockedY + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (errors.length() > 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
|
||||
if (autoPickup && !itemsToPickup.isEmpty()) {
|
||||
if (itemsToPickup.size() > MAX_AUTO_PICKUP_ITEMS) {
|
||||
LOGGER.warn("Floorplan auto-pickup rejected (over cap): user={} room={} itemCount={} cap={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), MAX_AUTO_PICKUP_ITEMS);
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
|
||||
"Too many items would be picked up (" + itemsToPickup.size() + " > " + MAX_AUTO_PICKUP_ITEMS + "). Remove some furniture manually and save again."));
|
||||
return;
|
||||
}
|
||||
|
||||
RoomLayout layout = room.getLayout();
|
||||
|
||||
if (layout instanceof CustomRoomLayout) {
|
||||
layout.setDoorX((short) doorX);
|
||||
layout.setDoorY((short) doorY);
|
||||
layout.setDoorDirection(doorRotation);
|
||||
layout.setHeightmap(map);
|
||||
layout.parse();
|
||||
|
||||
if (layout.getDoorTile() == null) {
|
||||
this.client.getHabbo().alert("Error");
|
||||
((CustomRoomLayout) layout).needsUpdate(false);
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
return;
|
||||
}
|
||||
((CustomRoomLayout) layout).needsUpdate(true);
|
||||
Emulator.getThreading().run((CustomRoomLayout) layout);
|
||||
} else {
|
||||
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
|
||||
Map<Integer, ArrayList<HabboItem>> byOwner = new HashMap<>();
|
||||
for (HabboItem itm : itemsToPickup) {
|
||||
if (itm == null) continue;
|
||||
byOwner.computeIfAbsent(itm.getUserId(), k -> new ArrayList<>()).add(itm);
|
||||
room.pickUpItem(itm, null);
|
||||
}
|
||||
|
||||
if (layout != null) {
|
||||
room.setHasCustomLayout(true);
|
||||
room.setNeedsUpdate(true);
|
||||
room.setLayout(layout);
|
||||
room.setWallSize(wallSize);
|
||||
room.setFloorSize(floorSize);
|
||||
room.setWallHeight(wallHeight);
|
||||
room.save();
|
||||
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
|
||||
habbos.addAll(room.getHabbos());
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
|
||||
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
|
||||
for (Habbo habbo : habbos) {
|
||||
habbo.getClient().sendResponse(message);
|
||||
for (Map.Entry<Integer, ArrayList<HabboItem>> entry : byOwner.entrySet()) {
|
||||
Habbo owner = Emulator.getGameEnvironment().getHabboManager().getHabbo(entry.getKey());
|
||||
if (owner == null) continue;
|
||||
for (HabboItem itm : entry.getValue()) {
|
||||
owner.getClient().sendResponse(new AddHabboItemComposer(itm));
|
||||
}
|
||||
owner.getClient().sendResponse(new InventoryRefreshComposer());
|
||||
}
|
||||
|
||||
LOGGER.info("Floorplan auto-pickup: user={} room={} itemCount={} owners={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), byOwner.size());
|
||||
}
|
||||
|
||||
RoomLayout layout = room.getLayout();
|
||||
|
||||
if (layout instanceof CustomRoomLayout) {
|
||||
layout.setDoorX((short) doorX);
|
||||
layout.setDoorY((short) doorY);
|
||||
layout.setDoorDirection(doorRotation);
|
||||
layout.setHeightmap(map);
|
||||
layout.parse();
|
||||
|
||||
if (layout.getDoorTile() == null) {
|
||||
this.client.getHabbo().alert("Error");
|
||||
((CustomRoomLayout) layout).needsUpdate(false);
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
return;
|
||||
}
|
||||
((CustomRoomLayout) layout).needsUpdate(true);
|
||||
Emulator.getThreading().run((CustomRoomLayout) layout);
|
||||
} else {
|
||||
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
|
||||
}
|
||||
|
||||
if (layout != null) {
|
||||
room.setHasCustomLayout(true);
|
||||
room.setNeedsUpdate(true);
|
||||
room.setLayout(layout);
|
||||
room.setWallSize(wallSize);
|
||||
room.setFloorSize(floorSize);
|
||||
room.setWallHeight(wallHeight);
|
||||
room.save();
|
||||
|
||||
this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp = now;
|
||||
LOGGER.info("Floorplan saved: user={} room={} mapLen={} rows={} cols={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map.length(), mapRows.length, firstRowSize);
|
||||
|
||||
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
|
||||
habbos.addAll(room.getHabbos());
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
|
||||
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
|
||||
for (Habbo habbo : habbos) {
|
||||
habbo.getClient().sendResponse(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
-37
@@ -25,45 +25,55 @@ public class GuildAcceptMembershipEvent extends MessageHandler {
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
|
||||
if (guild == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GuildMember actorMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
|
||||
boolean canAccept = guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|
||||
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|
||||
|| (actorMember != null && (actorMember.getRank().equals(GuildRank.ADMIN) || actorMember.getRank().equals(GuildRank.OWNER)));
|
||||
|
||||
if (!canAccept) {
|
||||
return;
|
||||
}
|
||||
|
||||
GuildMember targetMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId);
|
||||
|
||||
if (targetMember == null) {
|
||||
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER));
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetMember.getRank().type != GuildRank.REQUESTED.type) {
|
||||
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (guild != null) {
|
||||
GuildMember groupMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
|
||||
if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|
||||
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|
||||
|| (groupMember != null && (groupMember.getRank().equals(GuildRank.ADMIN) || groupMember.getRank().equals(GuildRank.OWNER)))) {
|
||||
if (habbo != null) {
|
||||
if (habbo.getHabboStats().hasGuild(guild.getId())) {
|
||||
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
|
||||
return;
|
||||
} else {
|
||||
//Check the user has requested
|
||||
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo);
|
||||
if (member == null || member.getRank().type != GuildRank.REQUESTED.type) {
|
||||
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER));
|
||||
return;
|
||||
} else {
|
||||
GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo);
|
||||
Emulator.getPluginManager().fireEvent(event);
|
||||
if (!event.isCancelled()) {
|
||||
habbo.getHabboStats().addGuild(guild.getId());
|
||||
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, habbo.getHabboInfo().getId(), true);
|
||||
guild.decreaseRequestCount();
|
||||
guild.increaseMemberCount();
|
||||
this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
|
||||
Room room = habbo.getHabboInfo().getCurrentRoom();
|
||||
if (room != null) {
|
||||
if (room.getGuildId() == guildId) {
|
||||
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
|
||||
room.refreshRightsForHabbo(habbo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
|
||||
}
|
||||
GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo);
|
||||
Emulator.getPluginManager().fireEvent(event);
|
||||
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.getHabboStats().addGuild(guild.getId());
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
|
||||
guild.decreaseRequestCount();
|
||||
guild.increaseMemberCount();
|
||||
this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
|
||||
|
||||
if (habbo != null) {
|
||||
Room room = habbo.getHabboInfo().getCurrentRoom();
|
||||
if (room != null && room.getGuildId() == guildId) {
|
||||
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
|
||||
room.refreshRightsForHabbo(habbo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -29,6 +29,11 @@ public class GuildDeclineMembershipEvent extends MessageHandler {
|
||||
if (guild != null) {
|
||||
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
|
||||
if (userId == this.client.getHabbo().getHabboInfo().getId() || guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && (member.getRank().equals(GuildRank.ADMIN) || member.getRank().equals(GuildRank.OWNER))) || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) {
|
||||
GuildMember target = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId);
|
||||
if (target == null || target.getRank().type != GuildRank.REQUESTED.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
guild.decreaseRequestCount();
|
||||
Emulator.getGameEnvironment().getGuildManager().removeMember(guild, userId);
|
||||
this.client.sendResponse(new GuildMembersComposer(guild, Emulator.getGameEnvironment().getGuildManager().getGuildMembers(guild, 0, 0, ""), this.client.getHabbo(), 0, 0, "", true, Emulator.getGameEnvironment().getGuildManager().getGuildMembersCount(guild, 0, 0, "")));
|
||||
|
||||
+74
-69
@@ -30,20 +30,69 @@ public class RequestGuildBuyEvent extends MessageHandler {
|
||||
final String name = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
|
||||
final String description = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
|
||||
|
||||
if(name.length() > 29){
|
||||
if (name.length() == 0 || name.length() > 29) {
|
||||
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.INVALID_GUILD_NAME));
|
||||
return;
|
||||
}
|
||||
if(description.length() > 254){
|
||||
if (description.length() > 254) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (Emulator.getConfig().getBoolean("catalog.guild.hc_required", true) && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
|
||||
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.HC_REQUIRED));
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
|
||||
if (r == null) {
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
if (r.hasGuild() || r.getGuildId() != 0) {
|
||||
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (r.getOwnerId() != this.client.getHabbo().getHabboInfo().getId()) {
|
||||
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
|
||||
ScripterManager.scripterDetected(this.client, message);
|
||||
LOGGER.info(message);
|
||||
return;
|
||||
}
|
||||
|
||||
int colorOne = this.packet.readInt();
|
||||
int colorTwo = this.packet.readInt();
|
||||
|
||||
int count = this.packet.readInt();
|
||||
|
||||
StringBuilder badge = new StringBuilder();
|
||||
|
||||
byte base = 1;
|
||||
|
||||
while (base < count) {
|
||||
int id = this.packet.readInt();
|
||||
int color = this.packet.readInt();
|
||||
int pos = this.packet.readInt();
|
||||
|
||||
if (base == 1) {
|
||||
badge.append("b");
|
||||
} else {
|
||||
badge.append("s");
|
||||
}
|
||||
|
||||
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
|
||||
|
||||
base += 3;
|
||||
}
|
||||
|
||||
// Only charge the player once every step has been validated. Previously the
|
||||
// credits were deducted before the room was checked, so a purchase that
|
||||
// failed afterwards (missing room, room already used by a guild, not the
|
||||
// owner) still took the credits without ever creating the group.
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
|
||||
int guildPrice = Emulator.getConfig().getInt("catalog.guild.price");
|
||||
if (this.client.getHabbo().getHabboInfo().getCredits() >= guildPrice) {
|
||||
@@ -54,78 +103,34 @@ public class RequestGuildBuyEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
|
||||
|
||||
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
r.setGuild(guild.getId());
|
||||
r.removeAllRights();
|
||||
r.setNeedsUpdate(true);
|
||||
|
||||
if (r != null) {
|
||||
if (r.hasGuild()) {
|
||||
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE));
|
||||
return;
|
||||
Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
|
||||
|
||||
if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
|
||||
Emulator.getBadgeImager().generate(guild);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new PurchaseOKComposer());
|
||||
this.client.sendResponse(new GuildBoughtComposer(guild));
|
||||
r.refreshGuild(guild);
|
||||
|
||||
for (Habbo habbo : r.getHabbos()) {
|
||||
if (habbo.getClient() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()) {
|
||||
if (r.getGuildId() == 0) {
|
||||
int colorOne = this.packet.readInt();
|
||||
int colorTwo = this.packet.readInt();
|
||||
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
|
||||
|
||||
int count = this.packet.readInt();
|
||||
|
||||
StringBuilder badge = new StringBuilder();
|
||||
|
||||
byte base = 1;
|
||||
|
||||
while (base < count) {
|
||||
int id = this.packet.readInt();
|
||||
int color = this.packet.readInt();
|
||||
int pos = this.packet.readInt();
|
||||
|
||||
if (base == 1) {
|
||||
badge.append("b");
|
||||
} else {
|
||||
badge.append("s");
|
||||
}
|
||||
|
||||
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
|
||||
|
||||
base += 3;
|
||||
}
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
|
||||
|
||||
r.setGuild(guild.getId());
|
||||
r.removeAllRights();
|
||||
r.setNeedsUpdate(true);
|
||||
|
||||
Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
|
||||
|
||||
if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
|
||||
Emulator.getBadgeImager().generate(guild);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new PurchaseOKComposer());
|
||||
this.client.sendResponse(new GuildBoughtComposer(guild));
|
||||
r.refreshGuild(guild);
|
||||
|
||||
for (Habbo habbo : r.getHabbos()) {
|
||||
if (habbo.getClient() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
|
||||
|
||||
if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
|
||||
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
|
||||
}
|
||||
}
|
||||
|
||||
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
|
||||
}
|
||||
} else {
|
||||
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
|
||||
ScripterManager.scripterDetected(this.client, message);
|
||||
LOGGER.info(message);
|
||||
if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
|
||||
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
|
||||
}
|
||||
}
|
||||
|
||||
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -2,7 +2,11 @@ package com.eu.habbo.messages.incoming.guilds.forums;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.guilds.Guild;
|
||||
import com.eu.habbo.habbohotel.guilds.GuildMember;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
|
||||
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer;
|
||||
|
||||
public class GuildForumDataEvent extends MessageHandler {
|
||||
@@ -20,10 +24,18 @@ public class GuildForumDataEvent extends MessageHandler {
|
||||
if (guild == null) return;
|
||||
if (!guild.hasForum()) return;
|
||||
|
||||
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
|
||||
boolean staff = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q);
|
||||
|
||||
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, staff)) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
|
||||
|
||||
if (!Emulator.getGameEnvironment().getGuildManager().hasViewedForum(this.client.getHabbo().getHabboInfo().getId(), guildId)) {
|
||||
Emulator.getGameEnvironment().getGuildManager().addView(this.client.getHabbo().getHabboInfo().getId(), guildId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -2,7 +2,11 @@ package com.eu.habbo.messages.incoming.guilds.forums;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.guilds.Guild;
|
||||
import com.eu.habbo.habbohotel.guilds.GuildMember;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
|
||||
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer;
|
||||
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadsComposer;
|
||||
import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer;
|
||||
@@ -24,8 +28,15 @@ public class GuildForumThreadsEvent extends MessageHandler {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(404));
|
||||
return;
|
||||
}
|
||||
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
|
||||
boolean staff = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q);
|
||||
|
||||
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, staff)) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
|
||||
this.client.sendResponse(new GuildForumThreadsComposer(guild, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -38,7 +38,6 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify thread belongs to the requested guild
|
||||
if (thread.getGuildId() != guildId) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(403));
|
||||
return;
|
||||
@@ -47,6 +46,11 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
|
||||
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
|
||||
boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && member.getRank().equals(GuildRank.ADMIN)));
|
||||
|
||||
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, hasStaffPermissions)) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (thread.getState() != ForumThreadState.HIDDEN_BY_GUILD_ADMIN || hasStaffPermissions || isGuildAdministrator) {
|
||||
this.client.sendResponse(new GuildForumCommentsComposer(guildId, threadId, index, thread.getComments(limit, index)));
|
||||
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.eu.habbo.messages.incoming.handshake;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.messages.NoAuthMessage;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import org.slf4j.Logger;
|
||||
@@ -24,6 +25,15 @@ public class MachineIDEvent extends MessageHandler {
|
||||
|
||||
this.client.setMachineId(storedMachineId);
|
||||
|
||||
// Persist the machine fingerprint onto the user so machine/super bans can
|
||||
// target it (createOfflineUserBan copies users.machine_id). The Nitro client
|
||||
// sends this UniqueID packet right after the SSO ticket, so the Habbo is
|
||||
// normally already loaded by the time we get here.
|
||||
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
|
||||
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
|
||||
Emulator.getThreading().run(this.client.getHabbo());
|
||||
}
|
||||
|
||||
LOGGER.debug("Setting client MachineId to {}", storedMachineId);
|
||||
}
|
||||
}
|
||||
+10
-11
@@ -133,17 +133,10 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
this.client.setHabbo(habbo);
|
||||
this.client.setMachineId(habbo.getHabboInfo().getMachineID());
|
||||
|
||||
// Clear the SSO ticket now that session is resumed (prevent reuse)
|
||||
if (!Emulator.debugging) {
|
||||
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
java.sql.PreparedStatement stmt = conn.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
|
||||
stmt.setString(1, "");
|
||||
stmt.setInt(2, habbo.getHabboInfo().getId());
|
||||
stmt.execute();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to clear SSO ticket after session resume", e);
|
||||
}
|
||||
}
|
||||
// NB: NON svuotiamo il ticket SSO qui (vedi HabboManager.loadHabbo):
|
||||
// dietro Cloudflare il client ritenta la connessione con lo stesso
|
||||
// ticket, quindi deve restare valido fino alla scadenza TTL. Consumarlo
|
||||
// farebbe fallire i retry / l'hard-refresh con "non-existing SSO token".
|
||||
} else {
|
||||
// Normal login — load from database
|
||||
habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso);
|
||||
@@ -168,6 +161,12 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!");
|
||||
}
|
||||
|
||||
// If the machine fingerprint already arrived (UniqueID before login),
|
||||
// persist it so machine/super bans can target this user.
|
||||
if (this.client.getMachineId() != null && !this.client.getMachineId().isEmpty()) {
|
||||
this.client.getHabbo().getHabboInfo().setMachineID(this.client.getMachineId());
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(habbo);
|
||||
Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo);
|
||||
} catch (Exception e) {
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolBan;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolBanType;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Apply an arbitrary-duration account ban. Duration is taken in hours
|
||||
* from the wire and converted to seconds for ModToolManager.ban —
|
||||
* unlike ModToolSanctionBanEvent which only accepts the four fixed
|
||||
* Habbo-protocol banType buckets.
|
||||
*/
|
||||
public class HousekeepingBanUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.ban";
|
||||
private static final int SECONDS_IN_HOUR = 3600;
|
||||
// 100-year ceiling, matches ModToolSanctionBanEvent's permanent ban.
|
||||
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
int hours = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || hours <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
long durationLong = (long) hours * SECONDS_IN_HOUR;
|
||||
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
|
||||
|
||||
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager()
|
||||
.ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0);
|
||||
|
||||
if (bans == null || bans.isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ModToolBan doesn't expose the `bans` table autoinc id on the
|
||||
// object, so we return the target user id as the actionId — it's
|
||||
// the only stable handle the client can use until a dedicated
|
||||
// housekeeping_log row id supersedes it.
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Permanently delete a room. Mirrors the minimum-viable subset of
|
||||
* RequestDeleteRoomEvent: eject all users from the live room, dispose
|
||||
* + uncache, then DELETE FROM rooms. Pets/guild/custom-layout cleanup
|
||||
* is intentionally skipped on this slice — leftover rows in those
|
||||
* tables become orphans but don't crash the emulator; a follow-up
|
||||
* pass can cascade once we have a HK audit-log row to attach the
|
||||
* orphan-cleanup to.
|
||||
*/
|
||||
public class HousekeepingDeleteRoomEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.delete";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room != null) {
|
||||
room.ejectAll();
|
||||
room.preventUnloading = false;
|
||||
room.dispose();
|
||||
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM rooms WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, roomId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomDetailComposer;
|
||||
|
||||
public class HousekeepingFindRoomByIdEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingRoomDetailComposer(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// loadRoom covers both the in-memory cache (already-loaded rooms)
|
||||
// and the offline path (SELECT * FROM rooms WHERE id=?). Pass
|
||||
// false for loadData so we don't pull furni/bots/pets just to
|
||||
// render an HK panel summary — getOwnerName / getUserCount work
|
||||
// on the pre-loaded skeleton.
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
this.client.sendResponse(new HousekeepingRoomDetailComposer(room));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer;
|
||||
|
||||
public class HousekeepingFindUserByIdEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// HabboManager.getHabboInfo(int) returns the in-memory record for
|
||||
// online users and falls through to the offline SQL lookup
|
||||
// otherwise, so a single call covers both branches.
|
||||
HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId);
|
||||
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(info));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer;
|
||||
|
||||
public class HousekeepingFindUserByNameEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = this.packet.readString();
|
||||
|
||||
if (username == null || username.isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
||||
HabboInfo info = online != null ? online.getHabboInfo() : HabboManager.getOfflineHabboInfo(username);
|
||||
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(info));
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Force-close the session of an online user. Unlike kick (which only
|
||||
* removes them from the current room), this drops their socket. Equivalent
|
||||
* to /disconnect in command form but issued through the HK panel.
|
||||
*/
|
||||
public class HousekeepingForceDisconnectUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.disconnect";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (target == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
target.alert(reason);
|
||||
}
|
||||
|
||||
// ACK first so the action result lands before the target's socket
|
||||
// closes (otherwise an alerted user on the same emulator thread may
|
||||
// already be torn down when we try to write).
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
|
||||
target.disconnect();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
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.housekeeping.HousekeepingDashboardComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingGetDashboardEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int onlineUsers = Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().size();
|
||||
int activeRooms = 0;
|
||||
int totalUsers = 0;
|
||||
int totalRooms = 0;
|
||||
int pendingTickets = 0;
|
||||
int sanctionsLast24h = 0;
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
|
||||
// activeRooms = loaded rooms with at least one user
|
||||
try {
|
||||
for (var room : Emulator.getGameEnvironment().getRoomManager().getActiveRooms()) {
|
||||
if (room != null && room.getUserCount() > 0) activeRooms++;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// fall through with 0
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM users");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) totalUsers = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM rooms");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) totalRooms = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM support_tickets WHERE state = 0");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) pendingTickets = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM bans WHERE timestamp > ?")) {
|
||||
statement.setInt(1, now - (24 * 3600));
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) sanctionsLast24h = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// Surface 0s rather than failing the whole dashboard on a missing
|
||||
// optional table — the HK panel can render partial data.
|
||||
}
|
||||
|
||||
int uptime = (int) ((System.currentTimeMillis() - HOUSEKEEPING_BOOT_MILLIS) / 1000);
|
||||
String version = "Arcturus-Morningstar-Extended";
|
||||
|
||||
this.client.sendResponse(new HousekeepingDashboardComposer(
|
||||
onlineUsers,
|
||||
totalUsers,
|
||||
activeRooms,
|
||||
totalRooms,
|
||||
onlineUsers, // peakOnlineToday — not tracked, use current as best-effort
|
||||
onlineUsers, // peakOnlineAllTime — same
|
||||
pendingTickets,
|
||||
sanctionsLast24h,
|
||||
Math.max(uptime, 0),
|
||||
version
|
||||
));
|
||||
}
|
||||
|
||||
// Approximate uptime — captured at class-load time rather than emu startup
|
||||
// (Emulator.java doesn't expose a public startup timestamp). For HK panel
|
||||
// headline metrics this is close enough; if tighter accuracy is needed
|
||||
// later, plumb Emulator.startup through and read it here.
|
||||
private static final long HOUSEKEEPING_BOOT_MILLIS = System.currentTimeMillis();
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.give_credits";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int amount = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || amount == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
// giveCredits already pushes UserCreditsComposer and persists via the
|
||||
// standard HabboInfo write path; nothing extra needed for the online branch.
|
||||
online.giveCredits(amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, amount);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Generic non-credits currency grant. Wire field `currencyType`:
|
||||
* 0 => duckets / pixels, 5 => diamonds, 101 => seasonal-primary.
|
||||
* Online users go through Habbo.givePoints / givePixels which dispatches
|
||||
* a UserCurrencyComposer; offline goes straight to `users_currency`.
|
||||
*/
|
||||
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
private static final int CURRENCY_DUCKETS = 0;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int currencyType = this.packet.readInt();
|
||||
int amount = this.packet.readInt();
|
||||
|
||||
String actionKey = "user.give_currency_" + currencyType;
|
||||
|
||||
if (userId <= 0 || amount == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
// givePixels writes users_currency type=0 and ships UserCurrency;
|
||||
// givePoints(type, amount) is the generalised path for everything else.
|
||||
if (currencyType == CURRENCY_DUCKETS) {
|
||||
online.givePixels(amount);
|
||||
} else {
|
||||
online.givePoints(currencyType, amount);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, currencyType);
|
||||
statement.setInt(3, amount);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Grant a furni item (by items_base id) `quantity` times. Each row in
|
||||
* the `items` table is one furni unit; quantity > 1 just batches the
|
||||
* insert. The online user's HabboInventory isn't proactively refreshed
|
||||
* — they'll see the new items next time they open the hand inventory
|
||||
* (or after a relog).
|
||||
*/
|
||||
public class HousekeepingGrantItemEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.grant_item";
|
||||
private static final int MAX_QUANTITY_PER_CALL = 100;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int itemId = this.packet.readInt();
|
||||
int quantity = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || itemId <= 0 || quantity <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (quantity > MAX_QUANTITY_PER_CALL) {
|
||||
quantity = MAX_QUANTITY_PER_CALL;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data) VALUES (?, ?, '')")) {
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, itemId);
|
||||
statement.addBatch();
|
||||
}
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.economy_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
public class HousekeepingKickAllFromRoomEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.kick_all";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
room.ejectAll();
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Kick a user out of their current room. Mirrors ModToolManager.kick
|
||||
* (leave room + alert), but the legacy method gates on ACC_SUPPORTTOOL,
|
||||
* which would force HK operators to also hold the support-tool permission.
|
||||
* Replicating the few lines locally keeps the HK module self-gated on
|
||||
* ACC_HOUSEKEEPING.
|
||||
*/
|
||||
public class HousekeepingKickUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.kick";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (target == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.hasPermission(Permission.ACC_UNKICKABLE)) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.getHabboInfo().getCurrentRoom() != null) {
|
||||
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
|
||||
}
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
target.alert(reason);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
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.housekeeping.HousekeepingActionLogComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Read the housekeeping_log audit table. The table isn't part of the
|
||||
* base FullDatabase.sql yet — operators who want audit have to create
|
||||
* it once:
|
||||
*
|
||||
* CREATE TABLE IF NOT EXISTS `housekeeping_log` (
|
||||
* `id` INT NOT NULL AUTO_INCREMENT,
|
||||
* `timestamp` INT NOT NULL,
|
||||
* `actor_id` INT NOT NULL,
|
||||
* `actor_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
* `target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||
* `target_id` INT NOT NULL DEFAULT 0,
|
||||
* `target_label` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
* `action` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
* `detail` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
* `success` TINYINT NOT NULL DEFAULT 1,
|
||||
* PRIMARY KEY (`id`), KEY `timestamp` (`timestamp`)
|
||||
* ) ENGINE=InnoDB;
|
||||
*
|
||||
* If the table is missing we swallow the SQL error and return an empty
|
||||
* list — the panel just shows "no audit entries" instead of breaking.
|
||||
* Writing into the table is a follow-up: each HK handler will append
|
||||
* a row once the table exists; for now the listing is read-only.
|
||||
*/
|
||||
public class HousekeepingListActionLogEvent extends MessageHandler {
|
||||
private static final int HARD_LIMIT = 500;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
|
||||
|
||||
List<HousekeepingActionLogComposer.Row> rows = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT id, timestamp, actor_id, actor_name, target_type, target_id, target_label, action, detail, success " +
|
||||
"FROM housekeeping_log ORDER BY id DESC LIMIT ?")) {
|
||||
statement.setInt(1, limit);
|
||||
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
rows.add(new HousekeepingActionLogComposer.Row(
|
||||
rs.getInt("id"),
|
||||
rs.getInt("timestamp"),
|
||||
rs.getInt("actor_id"),
|
||||
rs.getString("actor_name"),
|
||||
rs.getString("target_type"),
|
||||
rs.getInt("target_id"),
|
||||
rs.getString("target_label"),
|
||||
rs.getString("action"),
|
||||
rs.getString("detail"),
|
||||
rs.getInt("success") == 1
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// table absent — return empty list, log via emu logger left to the panel UI
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionLogComposer(rows));
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Toggle room-wide mute. Habbo's Room.setMuted is boolean, not duration-
|
||||
* scoped, so the wire `minutes` arg picks the semantic: minutes==0 =>
|
||||
* unmute, minutes>0 => mute. An emulator-side scheduled unmute could
|
||||
* use the value as a timer, but for now the mute stays until the
|
||||
* operator unmutes manually — the minutes is reserved as a forward-
|
||||
* compat field on the wire.
|
||||
*/
|
||||
public class HousekeepingMuteRoomEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.mute";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
int minutes = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
room.setMuted(minutes > 0);
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Apply an arbitrary-duration in-room mute. Habbo.mute is a session-only
|
||||
* mute (it stores remaining seconds on the live Habbo object), so the
|
||||
* target must be online for the action to take effect — when the target
|
||||
* isn't online the handler returns ok=false with `user_offline` so the
|
||||
* UI can fall back to ModToolSanctionMute or surface a clear error.
|
||||
*/
|
||||
public class HousekeepingMuteUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.mute";
|
||||
private static final int SECONDS_IN_MINUTE = 60;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
int minutes = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || minutes <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (target == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
|
||||
return;
|
||||
}
|
||||
|
||||
target.mute(minutes * SECONDS_IN_MINUTE, false);
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
target.alert(reason);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Reset a user's password to a fresh random 12-character alphanumeric
|
||||
* string. Persists a BCrypt `$2a$` hash of the new password into
|
||||
* `users.password` (matches what `AuthHttpUtil.checkPassword` /
|
||||
* `SessionEndpoints` / `AccountChangeEndpoints` already write and read),
|
||||
* clears `auth_ticket` so any active session can't be re-used to bypass
|
||||
* the reset, and ships the PLAINTEXT new password back to the operator
|
||||
* in the action-result `message` so they can communicate it out-of-band.
|
||||
*/
|
||||
public class HousekeepingResetUserPasswordEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.reset_password";
|
||||
private static final String PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
||||
private static final int PASSWORD_LENGTH = 12;
|
||||
private static final int BCRYPT_COST = 10;
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
String plain = randomPassword();
|
||||
String hash;
|
||||
|
||||
try {
|
||||
hash = BCrypt.hashpw(plain, BCrypt.gensalt(BCRYPT_COST));
|
||||
} catch (IllegalArgumentException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.hash_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users SET password = ?, auth_ticket = '' WHERE id = ? LIMIT 1")) {
|
||||
statement.setString(1, hash);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Plaintext flows through `message` — the client surfaces it via the
|
||||
// status banner so the operator can read it once. SSL is on the
|
||||
// operator: the only secure transport for the WS is wss://.
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, plain));
|
||||
}
|
||||
|
||||
private static String randomPassword() {
|
||||
StringBuilder sb = new StringBuilder(PASSWORD_LENGTH);
|
||||
|
||||
for (int i = 0; i < PASSWORD_LENGTH; i++) {
|
||||
sb.append(PASSWORD_ALPHABET.charAt(RNG.nextInt(PASSWORD_ALPHABET.length())));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomState;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Toggle the room state between OPEN (open) and LOCKED (closed). The
|
||||
* client picks which transition it wants via the boolean — true => OPEN,
|
||||
* false => LOCKED. Persists state through `Room.save()` so the change
|
||||
* outlives an unload.
|
||||
*/
|
||||
public class HousekeepingRoomStateEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
boolean open = this.packet.readBoolean();
|
||||
String actionKey = open ? "room.open" : "room.close";
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
room.setState(open ? RoomState.OPEN : RoomState.LOCKED);
|
||||
room.save();
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomListComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Search rooms by name. `exactMatch=true` => `name = ?` (used by the
|
||||
* findByName autocomplete that wants a unique hit). `exactMatch=false`
|
||||
* => `name LIKE concat(?, '%')` (used by the prefix dropdown).
|
||||
*
|
||||
* Both branches go through the same packet because the wire shape is
|
||||
* identical — the client picks which mode it wants by toggling the
|
||||
* boolean.
|
||||
*/
|
||||
public class HousekeepingSearchRoomsEvent extends MessageHandler {
|
||||
private static final int HARD_LIMIT = 50;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = this.packet.readString();
|
||||
boolean exactMatch = this.packet.readBoolean();
|
||||
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
|
||||
|
||||
if (query == null) query = "";
|
||||
query = query.trim();
|
||||
|
||||
if (query.isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>()));
|
||||
return;
|
||||
}
|
||||
|
||||
String sql = exactMatch
|
||||
? "SELECT id FROM rooms WHERE name = ? LIMIT ?"
|
||||
: "SELECT id FROM rooms WHERE name LIKE ? LIMIT ?";
|
||||
|
||||
List<Room> rooms = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, exactMatch ? query : query + "%");
|
||||
statement.setInt(2, limit);
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(set.getInt("id"), false);
|
||||
|
||||
if (room != null) rooms.add(room);
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// fall through with whatever we collected before the failure
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingRoomListComposer(rooms));
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.StaffAlertWithLinkComposer;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Mirrors :ha — staff alert with sender attribution, broadcast to
|
||||
* every online user whose `blockStaffAlerts` flag isn't set. Composed
|
||||
* once and forwarded by reference (sendResponse compiles to the same
|
||||
* underlying buffer) so the broadcast is O(N habbos) wire writes,
|
||||
* not O(N) compose calls.
|
||||
*/
|
||||
public class HousekeepingSendHotelAlertEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "hotel.alert";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = this.packet.readString();
|
||||
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.alert_empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
String body = message + "\r\n-" + this.client.getHabbo().getHabboInfo().getUsername();
|
||||
ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose();
|
||||
|
||||
int reached = 0;
|
||||
|
||||
for (Map.Entry<Integer, Habbo> entry : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().entrySet()) {
|
||||
Habbo habbo = entry.getValue();
|
||||
|
||||
if (habbo == null || habbo.getClient() == null) continue;
|
||||
if (habbo.getHabboStats() != null && habbo.getHabboStats().blockStaffAlerts) continue;
|
||||
|
||||
habbo.getClient().sendResponse(broadcast);
|
||||
reached++;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, reached, ""));
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Extend a user's HC by `days`. Adds to the existing club_expire_timestamp
|
||||
* if it's still in the future, otherwise stretches from `now`. Days==0
|
||||
* means cancel the active subscription (timestamp clamped to `now`).
|
||||
*/
|
||||
public class HousekeepingSetHcSubscriptionEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.set_hc";
|
||||
private static final int SECONDS_IN_DAY = 24 * 3600;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int days = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || days < 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
int newExpire;
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (days == 0) {
|
||||
newExpire = now;
|
||||
} else if (online != null) {
|
||||
int current = online.getHabboStats().getClubExpireTimestamp();
|
||||
newExpire = (current > now ? current : now) + (days * SECONDS_IN_DAY);
|
||||
} else {
|
||||
newExpire = now + (days * SECONDS_IN_DAY); // best-effort offline; can't read previous expiry cheaply
|
||||
}
|
||||
|
||||
if (online != null) {
|
||||
online.getHabboStats().setClubExpireTimestamp(newExpire);
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET club_expire_timestamp = ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, newExpire);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
|
||||
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.set_rank";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int rankId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || rankId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
|
||||
|
||||
if (!permissions.rankExists(rankId)) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
Rank rank = permissions.getRank(rankId);
|
||||
|
||||
// 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.
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users SET rank = ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, rankId);
|
||||
statement.setInt(2, userId);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
online.getHabboInfo().setRank(rank);
|
||||
// Ship the refreshed permissions snapshot — same payload the
|
||||
// :update_permissions command emits when a rank is rebound.
|
||||
online.getClient().sendResponse(new UserPermissionsComposer(online));
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
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.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Apply an arbitrary-duration trade lock. Writes
|
||||
* `users_settings.trade_locked_until = now + hours*3600` so the lock
|
||||
* survives logout/login — that column is the canonical timestamp the
|
||||
* mod-tool user-info composer queries on. Online users also get their
|
||||
* in-memory HabboStats.allowTrade flag cleared so the lock takes
|
||||
* effect on the active session without waiting for a relog.
|
||||
*/
|
||||
public class HousekeepingTradeLockUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.trade_lock";
|
||||
private static final int SECONDS_IN_HOUR = 3600;
|
||||
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int hours = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
|
||||
if (userId <= 0 || hours <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
long durationLong = (long) hours * SECONDS_IN_HOUR;
|
||||
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
|
||||
int lockedUntil = Emulator.getIntUnixTimestamp() + duration;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, lockedUntil);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
online.getHabboStats().setAllowTrade(false);
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
online.alert(reason);
|
||||
}
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Transfer ownership of a room to a different user. Updates both
|
||||
* `rooms.owner_id` and `rooms.owner_name` so the cached owner name on
|
||||
* the navigator stays in sync without forcing a relog. The room is
|
||||
* touched via direct SQL rather than via Room.setOwnerId() because
|
||||
* the room may not be loaded.
|
||||
*/
|
||||
public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.transfer";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
int newOwnerId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0 || newOwnerId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
HabboInfo newOwner = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(newOwnerId);
|
||||
|
||||
if (newOwner == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.new_owner_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET owner_id = ?, owner_name = ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, newOwnerId);
|
||||
statement.setString(2, newOwner.getUsername());
|
||||
statement.setInt(3, roomId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
public class HousekeepingUnbanUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.unban";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId);
|
||||
|
||||
if (info == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ModToolManager.unban only takes a username; the SQL UPDATE
|
||||
// happens against active bans (ban_expire > now), so calling it
|
||||
// on a never-banned user is a benign no-op that returns false.
|
||||
boolean cleared = Emulator.getGameEnvironment().getModToolManager().unban(info.getUsername());
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, cleared, cleared ? userId : 0, cleared ? "" : "housekeeping.error.no_active_ban"));
|
||||
}
|
||||
}
|
||||
+5
@@ -6,6 +6,11 @@ import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
|
||||
|
||||
public class DeletePrefixEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int prefixId = this.packet.readInt();
|
||||
|
||||
+132
-58
@@ -1,15 +1,20 @@
|
||||
package com.eu.habbo.messages.incoming.inventory.prefixes;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.WordFilterWord;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.UserPrefix;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
|
||||
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.prefixes.CustomPrefixPurchaseFailedComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
|
||||
import com.eu.habbo.messages.outgoing.users.UserCreditsComposer;
|
||||
import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -17,10 +22,18 @@ import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PurchasePrefixEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class);
|
||||
private static final String[] ALLOWED_FONTS = { "", "pixel", "cherry", "vampiro" };
|
||||
private static final String[] ALLOWED_EFFECTS = {
|
||||
"", "glow", "shadow", "italic", "outline", "underline", "pulse", "bounce", "wave", "shake",
|
||||
"discord-neon", "cartoon", "toon", "pop", "bold-glow", "rainbow", "frost", "gold", "glitch",
|
||||
"fire", "matrix", "sparkle"
|
||||
};
|
||||
private static final int MAX_ICON_LENGTH = 16;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
@@ -39,81 +52,101 @@ public class PurchasePrefixEvent extends MessageHandler {
|
||||
|
||||
if (habbo == null) return;
|
||||
|
||||
// Load settings
|
||||
int maxLength = getSettingInt("max_length", 15);
|
||||
int minRank = getSettingInt("min_rank_to_buy", 1);
|
||||
int priceCredits = getSettingInt("price_credits", 5);
|
||||
int pricePoints = getSettingInt("price_points", 0);
|
||||
int pointsType = getSettingInt("points_type", 0);
|
||||
int fontPriceCredits = getSettingInt("font_price_credits", 10);
|
||||
int fontPricePoints = getSettingInt("font_price_points", 0);
|
||||
int fontPointsType = getSettingInt("font_points_type", pointsType);
|
||||
Map<String, Integer> settings = loadSettings();
|
||||
int maxLength = setting(settings, "max_length", 15);
|
||||
int minRank = setting(settings, "min_rank_to_buy", 1);
|
||||
int priceCredits = setting(settings, "price_credits", 5);
|
||||
int pricePoints = setting(settings, "price_points", 0);
|
||||
int pointsType = setting(settings, "points_type", 0);
|
||||
int fontPriceCredits = setting(settings, "font_price_credits", 10);
|
||||
int fontPricePoints = setting(settings, "font_price_points", 0);
|
||||
int fontPointsType = setting(settings, "font_points_type", pointsType);
|
||||
int maxPrefixes = setting(settings, "max_prefixes", 60);
|
||||
|
||||
// Validate text
|
||||
text = text.trim();
|
||||
|
||||
if (text.isEmpty() || text.length() > maxLength) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Prefix text is invalid or too long (max " + maxLength + " characters)."));
|
||||
if (maxPrefixes > 0 && habbo.getInventory().getPrefixesComponent().getPrefixes().size() >= maxPrefixes) {
|
||||
this.fail(habbo, "You already own the maximum number of prefixes (" + maxPrefixes + ").");
|
||||
return;
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
|
||||
if (text.isEmpty() || text.length() > maxLength) {
|
||||
this.fail(habbo, "Prefix text is invalid or too long (max " + maxLength + " characters).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (containsControlChars(text)) {
|
||||
this.fail(habbo, "Prefix text contains invalid characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (containsFilteredWord(text)) {
|
||||
this.fail(habbo, "This prefix contains a blocked word.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate color (single hex or comma-separated multi hex for per-letter colors)
|
||||
String[] colorParts = color.split(",");
|
||||
|
||||
if (colorParts.length > text.length()) {
|
||||
this.fail(habbo, "Invalid color format.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (String part : colorParts) {
|
||||
if (!part.matches("^#[0-9A-Fa-f]{6}$")) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid color format."));
|
||||
this.fail(habbo, "Invalid color format.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check rank
|
||||
if (habbo.getHabboInfo().getRank().getId() < minRank) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Your rank is too low to purchase prefixes."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check blacklist
|
||||
if (isBlacklisted(text)) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "This prefix contains a blocked word."));
|
||||
this.fail(habbo, "Your rank is too low to purchase prefixes.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (icon == null) icon = "";
|
||||
icon = icon.trim();
|
||||
|
||||
if (!isValidIcon(icon)) {
|
||||
this.fail(habbo, "Invalid prefix icon.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect == null) effect = "";
|
||||
effect = effect.trim();
|
||||
effect = effect.trim().toLowerCase();
|
||||
|
||||
if (!isAllowedEffect(effect)) {
|
||||
this.fail(habbo, "Invalid prefix effect.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (font == null) font = "";
|
||||
font = font.trim().toLowerCase();
|
||||
|
||||
if (!isAllowedFont(font)) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid font format."));
|
||||
this.fail(habbo, "Invalid font format.");
|
||||
return;
|
||||
}
|
||||
|
||||
int totalPriceCredits = priceCredits + (!font.isEmpty() ? fontPriceCredits : 0);
|
||||
|
||||
// Check credits
|
||||
if (totalPriceCredits > 0 && habbo.getHabboInfo().getCredits() < totalPriceCredits) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits."));
|
||||
this.fail(habbo, "Not enough credits.");
|
||||
return;
|
||||
}
|
||||
|
||||
int totalPricePointsSameType = pricePoints + ((fontPricePoints > 0 && fontPointsType == pointsType && !font.isEmpty()) ? fontPricePoints : 0);
|
||||
|
||||
// Check points
|
||||
if (totalPricePointsSameType > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < totalPricePointsSameType) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
|
||||
this.fail(habbo, "Not enough points.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType && habbo.getHabboInfo().getCurrencyAmount(fontPointsType) < fontPricePoints) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
|
||||
this.fail(habbo, "Not enough points.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduct currency
|
||||
if (totalPriceCredits > 0) {
|
||||
habbo.getHabboInfo().addCredits(-totalPriceCredits);
|
||||
this.client.sendResponse(new UserCreditsComposer(habbo));
|
||||
@@ -129,47 +162,57 @@ public class PurchasePrefixEvent extends MessageHandler {
|
||||
this.client.sendResponse(new UserCurrencyComposer(habbo));
|
||||
}
|
||||
|
||||
// Create prefix
|
||||
int storedPoints = totalPricePointsSameType;
|
||||
int storedPointsType = (storedPoints > 0) ? pointsType : ((!font.isEmpty() && fontPricePoints > 0) ? fontPointsType : pointsType);
|
||||
|
||||
UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect, font, 0, text, storedPoints, storedPointsType, true);
|
||||
prefix.run(); // Insert into DB synchronously to get the ID
|
||||
habbo.getInventory().getPrefixesComponent().addPrefix(prefix);
|
||||
|
||||
habbo.getInventory().getPrefixesComponent().setActive(prefix.getId());
|
||||
this.client.sendResponse(new PrefixReceivedComposer(prefix));
|
||||
this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix));
|
||||
this.client.sendResponse(new UserNickIconsComposer(habbo));
|
||||
}
|
||||
|
||||
private int getSettingInt(String key, int defaultValue) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT `value` FROM custom_prefix_settings WHERE key_name = ?")) {
|
||||
statement.setString(1, key);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) {
|
||||
return Integer.parseInt(set.getString("value"));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | NumberFormatException e) {
|
||||
LOGGER.error("Error reading prefix setting: " + key, e);
|
||||
if (habbo.getHabboInfo().getCurrentRoom() != null) {
|
||||
habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private boolean isBlacklisted(String text) {
|
||||
String lowerText = text.toLowerCase();
|
||||
private void fail(Habbo habbo, String message) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, message));
|
||||
this.client.sendResponse(new CustomPrefixPurchaseFailedComposer(message));
|
||||
}
|
||||
|
||||
private Map<String, Integer> loadSettings() {
|
||||
Map<String, Integer> settings = new HashMap<>();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist")) {
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
if (lowerText.contains(set.getString("word").toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT key_name, `value` FROM custom_prefix_settings");
|
||||
ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
try {
|
||||
settings.put(set.getString("key_name"), Integer.parseInt(set.getString("value")));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Error checking prefix blacklist", e);
|
||||
LOGGER.error("Error reading prefix settings", e);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
private int setting(Map<String, Integer> settings, String key, int defaultValue) {
|
||||
Integer value = settings.get(key);
|
||||
return value != null ? value : defaultValue;
|
||||
}
|
||||
|
||||
private boolean containsFilteredWord(String text) {
|
||||
if (text == null || text.isEmpty()) return false;
|
||||
|
||||
for (WordFilterWord word : Emulator.getGameEnvironment().getWordFilter().getWords()) {
|
||||
if (word.key != null && !word.key.isEmpty() && StringUtils.containsIgnoreCase(text, word.key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -182,4 +225,35 @@ public class PurchasePrefixEvent extends MessageHandler {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAllowedEffect(String effect) {
|
||||
for (String allowedEffect : ALLOWED_EFFECTS) {
|
||||
if (allowedEffect.equals(effect)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isValidIcon(String icon) {
|
||||
if (icon.isEmpty()) return true;
|
||||
if (icon.length() > MAX_ICON_LENGTH) return false;
|
||||
|
||||
for (int i = 0; i < icon.length(); i++) {
|
||||
char c = icon.charAt(i);
|
||||
if (c < 0x20 || c == 0x7F || c == '<' || c == '>') return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean containsControlChars(String text) {
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c < 0x20 || c == 0x7F) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -4,6 +4,11 @@ import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
|
||||
|
||||
public class RequestUserPrefixesEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
this.client.sendResponse(new UserPrefixesComposer(this.client.getHabbo()));
|
||||
|
||||
+6
-1
@@ -2,11 +2,16 @@ package com.eu.habbo.messages.incoming.inventory.prefixes;
|
||||
|
||||
import com.eu.habbo.habbohotel.users.UserPrefix;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
|
||||
|
||||
public class SetActivePrefixEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int prefixId = this.packet.readInt();
|
||||
|
||||
+6
-1
@@ -7,6 +7,11 @@ import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
|
||||
|
||||
public class SetDisplayOrderEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
@@ -23,4 +28,4 @@ public class SetDisplayOrderEvent extends MessageHandler {
|
||||
habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.eu.habbo.messages.incoming.mentions;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.mentions.MentionManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
|
||||
public class DeleteMentionEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client == null || this.client.getHabbo() == null) return;
|
||||
|
||||
int userId = this.client.getHabbo().getHabboInfo().getId();
|
||||
int mentionId = this.packet.readInt();
|
||||
|
||||
if (mentionId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
|
||||
|
||||
if (!manager.tryAcquireDelete(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.delete(userId, mentionId);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.eu.habbo.messages.incoming.mentions;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.mentions.MentionManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
|
||||
public class MarkMentionsReadEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client == null || this.client.getHabbo() == null) return;
|
||||
|
||||
int userId = this.client.getHabbo().getHabboInfo().getId();
|
||||
int mode = this.packet.readInt();
|
||||
int mentionId = this.packet.readInt();
|
||||
|
||||
// Only mode 0 (mark-all) and mode 1 (mark-single) are valid. Reject
|
||||
// anything else so a crafted packet can't fall into the mark-all branch
|
||||
// by accident.
|
||||
if (mode != 0 && mode != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == 1 && mentionId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
|
||||
|
||||
if (!manager.tryAcquireMarkRead(userId, mode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.markRead(userId, mode, mentionId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.eu.habbo.messages.incoming.mentions;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.mentions.HabboMention;
|
||||
import com.eu.habbo.habbohotel.mentions.MentionManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.mentions.MentionsListComposer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RequestMentionsEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client == null || this.client.getHabbo() == null) return;
|
||||
|
||||
int userId = this.client.getHabbo().getHabboInfo().getId();
|
||||
|
||||
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
|
||||
|
||||
if (!manager.tryAcquireRequestList(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int limit = Emulator.getConfig().getInt("mentions.store.limit", 50);
|
||||
|
||||
List<HabboMention> mentions = manager.getMentions(userId, limit);
|
||||
this.client.sendResponse(new MentionsListComposer(mentions));
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.eu.habbo.messages.incoming.rarevalues;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.rarevalues.RareValuesComposer;
|
||||
|
||||
public class RequestRareValuesEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 5000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo() == null) return;
|
||||
|
||||
CatalogManager catalog = Emulator.getGameEnvironment().getCatalogManager();
|
||||
byte[] snapshot = catalog.getRareValuesPayloadSnapshot();
|
||||
if (snapshot != null) {
|
||||
this.client.sendResponse(new RareValuesComposer(snapshot));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new RareValuesComposer(catalog.getFurnitureValues()));
|
||||
}
|
||||
}
|
||||
+22
-3
@@ -5,11 +5,13 @@ import com.eu.habbo.habbohotel.bots.Bot;
|
||||
import com.eu.habbo.habbohotel.bots.BotManager;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomUserRotation;
|
||||
import com.eu.habbo.habbohotel.users.DanceType;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BotErrorComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDanceComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserNameChangedComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUsersComposer;
|
||||
import com.eu.habbo.plugin.events.bots.BotSavedChatEvent;
|
||||
import com.eu.habbo.plugin.events.bots.BotSavedLookEvent;
|
||||
@@ -28,13 +30,20 @@ public class BotSaveSettingsEvent extends MessageHandler {
|
||||
|
||||
if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
|
||||
int botId = this.packet.readInt();
|
||||
|
||||
Bot bot = room.getBot(Math.abs(botId));
|
||||
|
||||
if (bot == null)
|
||||
return;
|
||||
|
||||
int settingId = this.packet.readInt();
|
||||
boolean allowed = false;
|
||||
for (short a : bot.getOwnerActionIds()) {
|
||||
if (a == settingId) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allowed) return;
|
||||
|
||||
if (!bot.tryAcquireOwnerActionSlot()) return;
|
||||
|
||||
switch (settingId) {
|
||||
case 1:
|
||||
@@ -160,8 +169,18 @@ public class BotSaveSettingsEvent extends MessageHandler {
|
||||
bot.needsUpdate(true);
|
||||
room.sendComposer(new RoomUsersComposer(bot).compose());
|
||||
break;
|
||||
case Bot.ACTION_ROTATE:
|
||||
if (bot.getRoomUnit() == null) break;
|
||||
int next = (bot.getRoomUnit().getBodyRotation().getValue() + 2) % 8;
|
||||
RoomUserRotation rotation = RoomUserRotation.fromValue(next);
|
||||
bot.getRoomUnit().setRotation(rotation);
|
||||
bot.needsUpdate(true);
|
||||
room.sendComposer(new RoomUserStatusComposer(bot.getRoomUnit()).compose());
|
||||
break;
|
||||
}
|
||||
|
||||
bot.onPostOwnerAction(settingId);
|
||||
|
||||
if (bot.needsUpdate()) {
|
||||
Emulator.getThreading().run(bot);
|
||||
}
|
||||
|
||||
+3
@@ -34,6 +34,9 @@ public class RoomUserShoutEvent extends MessageHandler {
|
||||
if (RoomChatMessage.SAVE_ROOM_CHATS) {
|
||||
Emulator.getThreading().run(message);
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getMentionManager()
|
||||
.process(this.client.getHabbo(), this.client.getHabbo().getHabboInfo().getCurrentRoom(), message.getMessage(), RoomChatType.SHOUT);
|
||||
}
|
||||
} else {
|
||||
String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + "");
|
||||
|
||||
+3
@@ -36,6 +36,9 @@ public class RoomUserTalkEvent extends MessageHandler {
|
||||
if (RoomChatMessage.SAVE_ROOM_CHATS) {
|
||||
Emulator.getThreading().run(message);
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getMentionManager()
|
||||
.process(this.client.getHabbo(), room, message.getMessage(), RoomChatType.TALK);
|
||||
}
|
||||
} else {
|
||||
String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + "");
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.eu.habbo.messages.incoming.soundboard;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.soundboard.SoundboardSound;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.soundboard.SoundboardPlayComposer;
|
||||
|
||||
public class SoundboardPlayEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 250;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
if (habbo == null) return;
|
||||
|
||||
Room room = this.currentRoom();
|
||||
if (room == null || !room.isSoundboardEnabled()) return;
|
||||
|
||||
int soundId = this.packet.readInt();
|
||||
SoundboardSound sound = Emulator.getGameEnvironment().getSoundboardManager().getSound(soundId);
|
||||
if (sound == null) return;
|
||||
|
||||
// Broadcast to everyone in the room.
|
||||
room.sendComposer(new SoundboardPlayComposer(sound.id, sound.url, habbo.getHabboInfo().getUsername()).compose());
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.eu.habbo.messages.incoming.soundboard;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer;
|
||||
|
||||
public class SoundboardSetEnabledEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
if (habbo == null) return;
|
||||
|
||||
Room room = this.currentRoom();
|
||||
if (room == null) return;
|
||||
|
||||
// Only the room owner (or staff) may toggle the soundboard for the room.
|
||||
boolean isOwner = room.getOwnerId() == habbo.getHabboInfo().getId();
|
||||
if (!isOwner && !habbo.hasPermission(Permission.ACC_SUPPORTTOOL)) return;
|
||||
|
||||
boolean enabled = this.packet.readInt() == 1;
|
||||
|
||||
room.setSoundboardEnabled(enabled);
|
||||
Emulator.getGameEnvironment().getSoundboardManager().setRoomEnabled(room.getId(), enabled);
|
||||
|
||||
// Push the refreshed settings (flag + sound list) to everyone in the room
|
||||
// so the toolbar icon appears/disappears live.
|
||||
room.sendComposer(new SoundboardSettingsComposer(enabled, Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose());
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.eu.habbo.messages.incoming.wheel;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
|
||||
|
||||
public class WheelAdminGetPrizesEvent extends MessageHandler {
|
||||
public static final String PERMISSION_KEY = "acc_wheeladmin";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(PERMISSION_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new WheelAdminPrizesComposer(
|
||||
Emulator.getGameEnvironment().getWheelManager().getPrizes()));
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.eu.habbo.messages.incoming.wheel;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.wheel.WheelManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class WheelAdminSavePrizesEvent extends MessageHandler {
|
||||
public static final String PERMISSION_KEY = "acc_wheeladmin";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(PERMISSION_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
|
||||
|
||||
int count = this.packet.readInt();
|
||||
if (count <= 0 || count > WheelManager.MAX_PRIZES_PER_SAVE) return;
|
||||
|
||||
// The client sends the full authoritative list of prizes in display
|
||||
// order. id <= 0 means "insert a new prize"; any existing prize whose
|
||||
// id is absent from this list was removed in the editor and gets
|
||||
// soft-disabled below.
|
||||
Set<Integer> keptIds = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
int id = this.packet.readInt();
|
||||
String type = this.packet.readString();
|
||||
String value = this.packet.readString();
|
||||
int amount = this.packet.readInt();
|
||||
int pointsType = this.packet.readInt();
|
||||
int weight = this.packet.readInt();
|
||||
String label = this.packet.readString();
|
||||
int savedId = wheel.savePrize(id, type, value, amount, pointsType, weight, label, i);
|
||||
if (savedId > 0) keptIds.add(savedId);
|
||||
}
|
||||
|
||||
wheel.disablePrizesNotIn(keptIds);
|
||||
wheel.reload();
|
||||
|
||||
this.client.sendResponse(new WheelAdminPrizesComposer(wheel.getPrizes()));
|
||||
this.client.sendResponse(new WheelDataComposer(
|
||||
wheel.getUserState(this.client.getHabbo().getHabboInfo().getId()),
|
||||
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.eu.habbo.messages.incoming.wheel;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.wheel.WheelManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
|
||||
|
||||
public class WheelBuySpinEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
if (habbo == null) return;
|
||||
|
||||
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
|
||||
wheel.buySpin(habbo); // whether or not it succeeds, resend the balance
|
||||
|
||||
this.client.sendResponse(new WheelDataComposer(
|
||||
wheel.getUserState(habbo.getHabboInfo().getId()),
|
||||
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.eu.habbo.messages.incoming.wheel;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.wheel.WheelManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer;
|
||||
|
||||
public class WheelOpenEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
if (habbo == null) return;
|
||||
|
||||
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
|
||||
this.client.sendResponse(new WheelDataComposer(
|
||||
wheel.getUserState(habbo.getHabboInfo().getId()),
|
||||
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
|
||||
this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eu.habbo.messages.incoming.wheel;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.wheel.WheelManager;
|
||||
import com.eu.habbo.habbohotel.wheel.WheelPrize;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer;
|
||||
import com.eu.habbo.messages.outgoing.wheel.WheelResultComposer;
|
||||
|
||||
public class WheelSpinEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
if (habbo == null) return;
|
||||
|
||||
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
|
||||
WheelPrize prize = wheel.spin(habbo);
|
||||
|
||||
if (prize != null) {
|
||||
this.client.sendResponse(new WheelResultComposer(prize.id));
|
||||
}
|
||||
|
||||
// Refresh the balance either way so the client unlocks the wheel.
|
||||
this.client.sendResponse(new WheelDataComposer(
|
||||
wheel.getUserState(habbo.getHabboInfo().getId()),
|
||||
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
|
||||
|
||||
if (prize != null) {
|
||||
this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,6 +579,7 @@ public class Outgoing {
|
||||
public static final int PrefixReceivedComposer = 7002;
|
||||
public static final int ActivePrefixUpdatedComposer = 7003;
|
||||
public static final int UserNickIconsComposer = 7004;
|
||||
public static final int CustomPrefixPurchaseFailedComposer = 7005;
|
||||
public static final int AvailableCommandsComposer = 4050;
|
||||
|
||||
// YouTube Room Broadcast
|
||||
@@ -586,4 +587,23 @@ public class Outgoing {
|
||||
public static final int YouTubeRoomWatchersComposer = 8002;
|
||||
public static final int YouTubeRoomSettingsComposer = 8003;
|
||||
|
||||
// Housekeeping (in-client admin panel) — IDs 9200..9299 reserved
|
||||
public static final int HousekeepingUserDetailComposer = 9200;
|
||||
public static final int HousekeepingActionResultComposer = 9201;
|
||||
public static final int HousekeepingRoomDetailComposer = 9202;
|
||||
public static final int HousekeepingRoomListComposer = 9203;
|
||||
public static final int HousekeepingDashboardComposer = 9204;
|
||||
public static final int HousekeepingActionLogComposer = 9205;
|
||||
|
||||
// Custom features — IDs 9400+ reserved
|
||||
public static final int RareValuesComposer = 9400;
|
||||
public static final int WheelDataComposer = 9401;
|
||||
public static final int WheelResultComposer = 9402;
|
||||
public static final int WheelRecentWinsComposer = 9403;
|
||||
public static final int WheelAdminPrizesComposer = 9404;
|
||||
public static final int SoundboardSettingsComposer = 9405;
|
||||
public static final int SoundboardPlayComposer = 9406;
|
||||
public static final int MentionReceivedComposer = 4801;
|
||||
public static final int MentionsListComposer = 4802;
|
||||
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HousekeepingActionLogComposer extends MessageComposer {
|
||||
public static class Row {
|
||||
public final int id;
|
||||
public final int timestamp;
|
||||
public final int actorId;
|
||||
public final String actorName;
|
||||
public final String targetType;
|
||||
public final int targetId;
|
||||
public final String targetLabel;
|
||||
public final String action;
|
||||
public final String detail;
|
||||
public final boolean success;
|
||||
|
||||
public Row(int id, int timestamp, int actorId, String actorName, String targetType, int targetId,
|
||||
String targetLabel, String action, String detail, boolean success) {
|
||||
this.id = id;
|
||||
this.timestamp = timestamp;
|
||||
this.actorId = actorId;
|
||||
this.actorName = actorName != null ? actorName : "";
|
||||
this.targetType = targetType != null ? targetType : "user";
|
||||
this.targetId = targetId;
|
||||
this.targetLabel = targetLabel != null ? targetLabel : "";
|
||||
this.action = action != null ? action : "";
|
||||
this.detail = detail != null ? detail : "";
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Row> rows;
|
||||
|
||||
public HousekeepingActionLogComposer(List<Row> rows) {
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingActionLogComposer);
|
||||
this.response.appendInt(this.rows != null ? this.rows.size() : 0);
|
||||
|
||||
if (this.rows != null) {
|
||||
for (Row r : this.rows) {
|
||||
this.response.appendInt(r.id);
|
||||
this.response.appendInt(r.timestamp);
|
||||
this.response.appendInt(r.actorId);
|
||||
this.response.appendString(r.actorName);
|
||||
this.response.appendString(r.targetType);
|
||||
this.response.appendInt(r.targetId);
|
||||
this.response.appendString(r.targetLabel);
|
||||
this.response.appendString(r.action);
|
||||
this.response.appendString(r.detail);
|
||||
this.response.appendBoolean(r.success);
|
||||
}
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
/**
|
||||
* Generic ack for any housekeeping action (ban, mute, kick, give-credits,
|
||||
* room-close, …). The client matches it back to the originating call via
|
||||
* the `actionKey` field, which lets multiple in-flight actions share the
|
||||
* same event stream without ordering bugs.
|
||||
*/
|
||||
public class HousekeepingActionResultComposer extends MessageComposer {
|
||||
private final String actionKey;
|
||||
private final boolean ok;
|
||||
private final int actionId;
|
||||
private final String message;
|
||||
|
||||
public HousekeepingActionResultComposer(String actionKey, boolean ok, int actionId, String message) {
|
||||
this.actionKey = actionKey != null ? actionKey : "";
|
||||
this.ok = ok;
|
||||
this.actionId = actionId;
|
||||
this.message = message != null ? message : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingActionResultComposer);
|
||||
this.response.appendString(this.actionKey);
|
||||
this.response.appendBoolean(this.ok);
|
||||
this.response.appendInt(this.actionId);
|
||||
this.response.appendString(this.message);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
public class HousekeepingDashboardComposer extends MessageComposer {
|
||||
private final int onlineUsers;
|
||||
private final int totalUsers;
|
||||
private final int activeRooms;
|
||||
private final int totalRooms;
|
||||
private final int peakOnlineToday;
|
||||
private final int peakOnlineAllTime;
|
||||
private final int pendingTickets;
|
||||
private final int sanctionsLast24h;
|
||||
private final int serverUptimeSeconds;
|
||||
private final String serverVersion;
|
||||
|
||||
public HousekeepingDashboardComposer(int onlineUsers, int totalUsers, int activeRooms, int totalRooms,
|
||||
int peakOnlineToday, int peakOnlineAllTime, int pendingTickets,
|
||||
int sanctionsLast24h, int serverUptimeSeconds, String serverVersion) {
|
||||
this.onlineUsers = onlineUsers;
|
||||
this.totalUsers = totalUsers;
|
||||
this.activeRooms = activeRooms;
|
||||
this.totalRooms = totalRooms;
|
||||
this.peakOnlineToday = peakOnlineToday;
|
||||
this.peakOnlineAllTime = peakOnlineAllTime;
|
||||
this.pendingTickets = pendingTickets;
|
||||
this.sanctionsLast24h = sanctionsLast24h;
|
||||
this.serverUptimeSeconds = serverUptimeSeconds;
|
||||
this.serverVersion = serverVersion != null ? serverVersion : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingDashboardComposer);
|
||||
this.response.appendInt(this.onlineUsers);
|
||||
this.response.appendInt(this.totalUsers);
|
||||
this.response.appendInt(this.activeRooms);
|
||||
this.response.appendInt(this.totalRooms);
|
||||
this.response.appendInt(this.peakOnlineToday);
|
||||
this.response.appendInt(this.peakOnlineAllTime);
|
||||
this.response.appendInt(this.pendingTickets);
|
||||
this.response.appendInt(this.sanctionsLast24h);
|
||||
this.response.appendInt(this.serverUptimeSeconds);
|
||||
this.response.appendString(this.serverVersion);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomState;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
public class HousekeepingRoomDetailComposer extends MessageComposer {
|
||||
private final Room room;
|
||||
|
||||
public HousekeepingRoomDetailComposer(Room room) {
|
||||
this.room = room;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingRoomDetailComposer);
|
||||
|
||||
if (this.room == null) {
|
||||
this.response.appendBoolean(false);
|
||||
return this.response;
|
||||
}
|
||||
|
||||
this.response.appendBoolean(true);
|
||||
appendRoomFields(this.response, this.room);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/** Shared by HousekeepingRoomListComposer too. */
|
||||
public static void appendRoomFields(ServerMessage response, Room room) {
|
||||
response.appendInt(room.getId());
|
||||
response.appendString(safe(room.getName()));
|
||||
response.appendString(safe(room.getDescription()));
|
||||
response.appendInt(room.getOwnerId());
|
||||
response.appendString(safe(room.getOwnerName()));
|
||||
response.appendInt(room.getUserCount());
|
||||
response.appendInt(room.getUsersMax());
|
||||
response.appendBoolean(room.getState() != null && room.getState() != RoomState.OPEN);
|
||||
response.appendBoolean(room.isMuted());
|
||||
response.appendBoolean(room.isPublicRoom());
|
||||
response.appendInt(0); // createdAt — Room doesn't expose; left as 0 until a schema-side timestamp surfaces.
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HousekeepingRoomListComposer extends MessageComposer {
|
||||
private final List<Room> rooms;
|
||||
|
||||
public HousekeepingRoomListComposer(List<Room> rooms) {
|
||||
this.rooms = rooms;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingRoomListComposer);
|
||||
this.response.appendInt(this.rooms != null ? this.rooms.size() : 0);
|
||||
|
||||
if (this.rooms != null) {
|
||||
for (Room room : this.rooms) {
|
||||
HousekeepingRoomDetailComposer.appendRoomFields(this.response, room);
|
||||
}
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user