Merge pull request #99 from Lorenzune/merge-duckie-main-2026-05-06

Merge live secure runtime updates into dev
This commit is contained in:
DuckieTM
2026-05-06 07:08:37 +02:00
committed by GitHub
87 changed files with 3898 additions and 291 deletions
-1
View File
@@ -987,4 +987,3 @@ ALTER TABLE `catalog_pages_bc`
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
+286 -12
View File
@@ -1,3 +1,13 @@
-- ============================================================
-- Custom Prefix System - Complete Setup (safe upgrade version)
-- ============================================================
-- Questo script è pensato per essere rieseguito senza errori
-- anche se le tabelle esistono già con una struttura parziale.
-- ------------------------------------------------------------
-- 1. Main user prefixes table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `user_prefixes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
@@ -5,28 +15,57 @@ CREATE TABLE IF NOT EXISTS `user_prefixes` (
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_user_active` (`user_id`, `active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. Prefix settings table
-- ------------------------------------------------------------
-- 2. Catalog table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 3. User visual settings
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
`user_id` INT(11) NOT NULL,
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 4. Prefix settings table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
`key_name` VARCHAR(100) NOT NULL,
`value` VARCHAR(255) NOT NULL,
PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default settings
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0');
-- 3. Blacklisted words table
-- ------------------------------------------------------------
-- 5. Blacklist table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
@@ -34,13 +73,249 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
UNIQUE KEY `uk_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Example blacklist entries (customize as needed)
-- ============================================================
-- Schema upgrades for existing installations
-- ============================================================
-- ------------------------------------------------------------
-- user_prefixes: add missing columns safely
-- ------------------------------------------------------------
SET @dbname = DATABASE();
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'font'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'catalog_prefix_id'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `catalog_prefix_id` INT(11) NOT NULL DEFAULT 0 AFTER `font`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'display_name'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `display_name` VARCHAR(100) NOT NULL DEFAULT '''' AFTER `catalog_prefix_id`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'points'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `display_name`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'points_type'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'is_custom'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `is_custom` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ------------------------------------------------------------
-- custom_prefixes_catalog: add missing columns safely
-- ------------------------------------------------------------
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'font'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'points'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `font`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'points_type'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'enabled'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'sort_order'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `sort_order` INT(11) NOT NULL DEFAULT 0 AFTER `enabled`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ============================================================
-- Default settings
-- ============================================================
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0'),
('font_price_credits', '10'),
('font_price_points', '0'),
('font_points_type', '0');
-- ============================================================
-- Default catalog entries
-- ============================================================
INSERT IGNORE INTO `custom_prefixes_catalog`
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
-- ============================================================
-- Example blacklist entries
-- ============================================================
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'),
('staff'),
('mod'),
('owner');
-- ============================================================
-- Notes
-- ============================================================
-- Preset prefixes for `:customize` are loaded directly by
-- UserNickIconsComposer and displayed inside the `:customize` panel.
--
-- This setup does not require rows in `catalog_pages`.
--
-- Command texts / permission inserts are intentionally omitted
-- for compatibility with both legacy and normalized permission schemas.
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command
('commands.keys.cmd_give_prefix', 'giveprefix'),
@@ -79,4 +354,3 @@ VALUES
('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_remove_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_prefix_blacklist', '1', '0', '0', '0', '0', '0', '0', '1');
@@ -0,0 +1,36 @@
-- ============================================================
-- Nick Icon Customization Setup
-- ============================================================
CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`icon_key` VARCHAR(50) NOT NULL,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_icon_key` (`icon_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `user_nick_icons` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`icon_key` VARCHAR(50) NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_icon` (`user_id`, `icon_key`),
KEY `idx_user_id` (`user_id`),
KEY `idx_user_active` (`user_id`, `active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
('1', 'Icon 1', 10, 0, 1, 1),
('2', 'Icon 2', 10, 0, 1, 2),
('3', 'Icon 3', 10, 0, 1, 3),
('4', 'Icon 4', 10, 0, 1, 4),
('5', 'Icon 5', 10, 0, 1, 5),
('6', 'Icon 6', 10, 0, 1, 6);
ALTER TABLE `custom_nick_icons_catalog`
ADD COLUMN IF NOT EXISTS `display_name` VARCHAR(100) NOT NULL DEFAULT '' AFTER `icon_key`;
@@ -0,0 +1,8 @@
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `remember_token_hash` VARCHAR(64) NOT NULL DEFAULT '' AFTER `auth_ticket`;
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `remember_token_expires_at` INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `remember_token_hash`;
ALTER TABLE `users`
ADD INDEX IF NOT EXISTS `idx_users_remember_token_hash` (`remember_token_hash`);
@@ -0,0 +1,3 @@
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`)
VALUES ('hotel.wired.message.max_length', '512', 'Maximum length of text fields used by wired messages and bot text effects.')
ON DUPLICATE KEY UPDATE `value` = '512';
+3 -3
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.1.2</version>
<version>4.1.13</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -163,7 +163,7 @@
<version>2.13.0</version>
</dependency>
<!-- jBCrypt — used by the built-in /api/auth/* HTTP login handler
<!-- jBCrypt — used by the built-in /api/auth/* HTTP login handler
to verify Laravel-style $2y$ BCrypt hashes from users.password -->
<dependency>
<groupId>org.mindrot</groupId>
@@ -171,7 +171,7 @@
<version>0.4</version>
</dependency>
<!-- Jakarta Mail — used by the built-in forgot-password endpoint
<!-- Jakarta Mail — used by the built-in forgot-password endpoint
when smtp.* keys are configured in emulator_settings -->
<dependency>
<groupId>org.eclipse.angus</groupId>
@@ -21,6 +21,7 @@ import com.eu.habbo.habbohotel.pets.PetManager;
import com.eu.habbo.habbohotel.polls.PollManager;
import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager;
import com.eu.habbo.habbohotel.rooms.RoomManager;
import com.eu.habbo.habbohotel.translations.GoogleTranslateManager;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
@@ -60,6 +61,7 @@ public class GameEnvironment {
private SubscriptionManager subscriptionManager;
private CalendarManager calendarManager;
private RoomChatBubbleManager roomChatBubbleManager;
private GoogleTranslateManager googleTranslateManager;
private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager;
@@ -88,6 +90,7 @@ public class GameEnvironment {
this.pollManager = new PollManager();
this.calendarManager = new CalendarManager();
this.roomChatBubbleManager = new RoomChatBubbleManager();
this.googleTranslateManager = new GoogleTranslateManager();
this.customBadgeManager = new CustomBadgeManager();
this.infostandBackgroundManager = new InfostandBackgroundManager();
@@ -127,6 +130,9 @@ public class GameEnvironment {
this.hotelViewManager.dispose();
this.subscriptionManager.dispose();
this.calendarManager.dispose();
if (this.googleTranslateManager != null) {
this.googleTranslateManager.clearCache();
}
LOGGER.info("GameEnvironment -> Disposed!");
}
@@ -226,6 +232,10 @@ public class GameEnvironment {
return roomChatBubbleManager;
}
public GoogleTranslateManager getGoogleTranslateManager() {
return this.googleTranslateManager;
}
public CustomBadgeManager getCustomBadgeManager() {
return this.customBadgeManager;
}
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.achievements;
import com.eu.habbo.Emulator;
import com.eu.habbo.database.SqlQueries;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge;
@@ -50,12 +49,16 @@ public class AchievementManager {
if (habbo != null) {
progressAchievement(habbo, achievement, amount);
} else {
try {
SqlQueries.update(
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) "
+ "ON DUPLICATE KEY UPDATE amount = amount + ?",
habboId, achievement.id, amount, amount);
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("" +
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE amount = amount + ?")) {
statement.setInt(1, habboId);
statement.setInt(2, achievement.id);
statement.setInt(3, amount);
statement.setInt(4, amount);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
@@ -200,41 +203,48 @@ public class AchievementManager {
}
public static void createUserEntry(Habbo habbo, Achievement achievement) {
try {
SqlQueries.update(
"INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)",
habbo.getHabboInfo().getId(), achievement.name, 1);
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)")) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setString(2, achievement.name);
statement.setInt(3, 1);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public static void saveAchievements(Habbo habbo) {
int userId = habbo.getHabboInfo().getId();
try {
SqlQueries.batchUpdate(
"UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1",
habbo.getHabboStats().getAchievementProgress().entrySet(),
(ps, entry) -> {
ps.setInt(1, entry.getValue());
ps.setString(2, entry.getKey().name);
ps.setInt(3, userId);
});
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1")) {
statement.setInt(3, habbo.getHabboInfo().getId());
for (Map.Entry<Achievement, Integer> map : habbo.getHabboStats().getAchievementProgress().entrySet()) {
statement.setInt(1, map.getValue());
statement.setString(2, map.getKey().name);
statement.addBatch();
}
statement.executeBatch();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public static int getAchievementProgressForHabbo(int userId, Achievement achievement) {
try {
return SqlQueries.queryOne(
"SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1",
rs -> rs.getInt("progress"),
userId, achievement.name).orElse(0);
} catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e);
if (achievement == null) {
return 0;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1")) {
statement.setInt(1, userId);
statement.setString(2, achievement.name);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return set.getInt("progress");
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
public void reload() {
@@ -71,13 +71,23 @@ public class BotManager {
}
public Bot createBot(THashMap<String, String> data, String type) {
return this.createBot(data, type, 0);
}
public Bot createBot(THashMap<String, String> data, String type, int ownerId) {
if (ownerId <= 0) {
LOGGER.error("Cannot create bot of type '{}' without a valid owner user id.", type);
return null;
}
Bot bot = null;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (0, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, data.get("name"));
statement.setString(2, data.get("motto"));
statement.setString(3, data.get("figure"));
statement.setString(4, data.get("gender").toUpperCase());
statement.setString(5, type);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (?, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, ownerId);
statement.setString(2, data.get("name"));
statement.setString(3, data.get("motto"));
statement.setString(4, data.get("figure"));
statement.setString(5, data.get("gender").toUpperCase());
statement.setString(6, type);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
@@ -1058,7 +1058,7 @@ public class CatalogManager {
}
}
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type);
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type, habbo.getHabboInfo().getId());
if (bot != null) {
bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId());
@@ -1,5 +1,6 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
@@ -11,10 +12,12 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.ServerMessage;
import gnu.trove.set.hash.THashSet;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private static final int COMPARISON_LESS_THAN = 0;
@@ -23,9 +26,16 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private static final int SOURCE_GROUP_USERS = 0;
private static final int SOURCE_GROUP_FURNI = 1;
private static final int SOURCE_USER_TRIGGER = 0;
private static final int SOURCE_USER_SIGNAL = 1;
private static final int SOURCE_USER_CLICKED = 2;
private static final int SOURCE_FURNI_TRIGGER = 3;
private static final int SOURCE_FURNI_PICKED = 4;
private static final int SOURCE_FURNI_SIGNAL = 5;
public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY;
private final THashSet<HabboItem> items;
private int comparison = COMPARISON_EQUAL;
private int quantity = 0;
private int sourceGroup = SOURCE_GROUP_USERS;
@@ -33,10 +43,12 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
this.items = new THashSet<>();
}
public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells);
this.items = new THashSet<>();
}
@Override
@@ -46,9 +58,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public void serializeWiredData(ServerMessage message, Room room) {
message.appendBoolean(false);
message.appendInt(5);
message.appendInt(0);
this.refresh(room);
boolean pickMode = this.sourceGroup == SOURCE_GROUP_FURNI && this.sourceType == WiredSourceUtil.SOURCE_SELECTED;
message.appendBoolean(pickMode);
message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION);
message.appendInt(pickMode ? this.items.size() : 0);
if (pickMode) {
for (HabboItem item : this.items) {
message.appendInt(item.getId());
}
}
message.appendInt(this.getBaseItem().getSpriteId());
message.appendInt(this.getId());
message.appendString("");
@@ -69,8 +90,36 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0;
this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS;
this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER;
this.items.clear();
if (params.length > 3) {
this.sourceGroup = this.normalizeSourceGroup(params[2]);
this.sourceType = this.normalizeSourceType(this.sourceGroup, params[3]);
} else {
this.setSourceSelection((params.length > 2) ? params[2] : SOURCE_USER_TRIGGER);
}
if (this.sourceGroup != SOURCE_GROUP_FURNI || this.sourceType != WiredSourceUtil.SOURCE_SELECTED) {
return true;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
return false;
}
int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) {
return false;
}
for (int itemId : settings.getFurniIds()) {
HabboItem item = room.getHabboItem(itemId);
if (item != null) {
this.items.add(item);
}
}
return true;
}
@@ -97,11 +146,14 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
return WiredManager.getGson().toJson(new JsonData(
this.comparison,
this.quantity,
this.sourceGroup,
this.sourceType
this.sourceType,
this.items.stream().map(HabboItem::getId).collect(Collectors.toList())
));
}
@@ -125,6 +177,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
this.quantity = this.normalizeQuantity(data.quantity);
this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup);
this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType);
this.loadSelectedItems(data.itemIds, room);
return;
}
@@ -150,6 +203,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public void onPickUp() {
this.items.clear();
this.comparison = COMPARISON_EQUAL;
this.quantity = 0;
this.sourceGroup = SOURCE_GROUP_USERS;
@@ -158,7 +212,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int resolveCount(WiredContext ctx) {
if (this.sourceGroup == SOURCE_GROUP_FURNI) {
List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, null);
List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, this.items);
return items.size();
}
@@ -188,10 +242,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int normalizeSourceType(int group, int value) {
if (group == SOURCE_GROUP_USERS) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
switch (value) {
case WiredSourceUtil.SOURCE_CLICKED_USER:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_SELECTOR:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
@@ -201,17 +263,104 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
}
}
private int getSourceSelection() {
if (this.sourceGroup == SOURCE_GROUP_FURNI) {
switch (this.sourceType) {
case WiredSourceUtil.SOURCE_SELECTED:
return SOURCE_FURNI_PICKED;
case WiredSourceUtil.SOURCE_SIGNAL:
return SOURCE_FURNI_SIGNAL;
default:
return SOURCE_FURNI_TRIGGER;
}
}
switch (this.sourceType) {
case WiredSourceUtil.SOURCE_CLICKED_USER:
return SOURCE_USER_CLICKED;
case WiredSourceUtil.SOURCE_SIGNAL:
return SOURCE_USER_SIGNAL;
default:
return SOURCE_USER_TRIGGER;
}
}
private void setSourceSelection(int value) {
switch (value) {
case SOURCE_USER_SIGNAL:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_SIGNAL;
break;
case SOURCE_USER_CLICKED:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_CLICKED_USER;
break;
case SOURCE_FURNI_TRIGGER:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_TRIGGER;
break;
case SOURCE_FURNI_PICKED:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_SELECTED;
break;
case SOURCE_FURNI_SIGNAL:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_SIGNAL;
break;
default:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_TRIGGER;
break;
}
}
private void loadSelectedItems(List<Integer> itemIds, Room room) {
this.items.clear();
if (itemIds == null || room == null) {
return;
}
for (Integer itemId : itemIds) {
HabboItem item = room.getHabboItem(itemId);
if (item != null) {
this.items.add(item);
}
}
}
private void refresh(Room room) {
if (room == null || this.items.isEmpty()) {
return;
}
THashSet<HabboItem> itemsToRemove = new THashSet<>();
for (HabboItem item : this.items) {
if (item == null || room.getHabboItem(item.getId()) == null) {
itemsToRemove.add(item);
}
}
for (HabboItem item : itemsToRemove) {
this.items.remove(item);
}
}
static class JsonData {
int comparison;
int quantity;
int sourceGroup;
int sourceType;
List<Integer> itemIds;
public JsonData(int comparison, int quantity, int sourceGroup, int sourceType) {
public JsonData(int comparison, int quantity, int sourceGroup, int sourceType, List<Integer> itemIds) {
this.comparison = comparison;
this.quantity = quantity;
this.sourceGroup = sourceGroup;
this.sourceType = sourceType;
this.itemIds = itemIds;
}
}
}
@@ -82,7 +82,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect {
this.setDelay(delay);
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100)));
this.mode = mode;
return true;
@@ -105,7 +105,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect {
throw new WiredSaveException("Delay too long");
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100)));
this.mode = mode;
this.setDelay(delay);
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer;
import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room;
@@ -60,12 +61,20 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
}
for (HabboItem item : effectiveItems) {
if (!(item instanceof InteractionGameUpCounter)) {
if (!(item instanceof InteractionGameTimer)) {
continue;
}
InteractionGameUpCounter counter = (InteractionGameUpCounter) item;
if (item instanceof InteractionGameUpCounter) {
this.controlUpCounter((InteractionGameUpCounter) item, room);
continue;
}
this.controlGameTimer((InteractionGameTimer) item, room);
}
}
private void controlUpCounter(InteractionGameUpCounter counter, Room room) {
switch (this.action) {
case ACTION_START:
counter.restartFromZero(room);
@@ -84,6 +93,43 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
break;
}
}
private void controlGameTimer(InteractionGameTimer timer, Room room) {
switch (this.action) {
case ACTION_START:
timer.startTimer(room);
break;
case ACTION_STOP:
this.stopGameTimer(timer, room, false);
break;
case ACTION_RESET:
this.stopGameTimer(timer, room, true);
break;
case ACTION_PAUSE:
timer.pauseTimer(room);
break;
case ACTION_RESUME:
timer.resumeTimer(room);
break;
}
}
private void stopGameTimer(InteractionGameTimer timer, Room room, boolean resetTime) {
boolean wasActive = timer.isRunning() || timer.isPaused();
timer.endGame(room);
if (resetTime) {
timer.setTimeNow(timer.getBaseTime());
timer.setExtradata(timer.getTimeNow() + "\t" + timer.getBaseTime());
}
room.updateItem(timer);
timer.needsUpdate(true);
if (wasActive) {
WiredManager.triggerGameEnds(room);
}
}
@Deprecated
@@ -206,7 +252,7 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
throw new WiredSaveException(String.format("Item %s not found", itemId));
}
if (!(item instanceof InteractionGameUpCounter)) {
if (!(item instanceof InteractionGameTimer)) {
throw new WiredSaveException("wiredfurni.error.require_counter_furni");
}
@@ -53,26 +53,37 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
return;
}
HabboItem moveItem = this.resolveLastMoveItem(ctx);
HabboItem targetItem = this.resolveLastTargetItem(ctx);
List<HabboItem> moveItems = this.resolveMoveItems(ctx);
List<HabboItem> targetItems = this.resolveTargetItems(ctx);
if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) {
if (moveItems.isEmpty() || targetItems.isEmpty()) {
return;
}
int targetIndex = 0;
for (HabboItem moveItem : moveItems) {
if (moveItem == null) {
continue;
}
HabboItem targetItem = targetItems.get(targetIndex % targetItems.size());
targetIndex++;
if (targetItem == null || moveItem.getId() == targetItem.getId()) {
continue;
}
RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY());
if (targetTile == null) {
return;
continue;
}
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
return;
continue;
}
error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
return;
WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx);
}
}
@@ -233,35 +244,23 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
return COOLDOWN_MOVEMENT;
}
private HabboItem resolveLastMoveItem(WiredContext ctx) {
return this.resolveLastItem(ctx, this.moveSource, this.moveItems);
private List<HabboItem> resolveMoveItems(WiredContext ctx) {
return this.resolveItems(ctx, this.moveSource, this.moveItems);
}
private HabboItem resolveLastTargetItem(WiredContext ctx) {
private List<HabboItem> resolveTargetItems(WiredContext ctx) {
int source = (this.targetSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.targetSource;
return this.resolveLastItem(ctx, source, this.targetItems);
return this.resolveItems(ctx, source, this.targetItems);
}
private HabboItem resolveLastItem(WiredContext ctx, int source, List<HabboItem> items) {
private List<HabboItem> resolveItems(WiredContext ctx, int source, List<HabboItem> items) {
if (source == WiredSourceUtil.SOURCE_SELECTED) {
this.validateItems(items);
}
List<HabboItem> resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items);
if (resolvedItems.isEmpty()) {
return null;
}
for (int index = resolvedItems.size() - 1; index >= 0; index--) {
HabboItem item = resolvedItems.get(index);
if (item != null) {
return item;
}
}
return null;
return WiredSourceUtil.resolveItems(ctx, source, items).stream()
.filter(item -> item != null && ctx.room().getHabboItem(item.getId()) != null)
.collect(Collectors.toList());
}
private List<HabboItem> parseItems(String data, Room room) throws WiredSaveException {
@@ -33,7 +33,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
public static final WiredEffectType type = WiredEffectType.SEND_SIGNAL;
private static final int MAX_SIGNAL_DEPTH = 10;
public static int MAX_SIGNAL_DEPTH = 100;
private static final int ANTENNA_PICKED = 0;
private static final int ANTENNA_TRIGGER = 1;
@@ -166,7 +166,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
.signalChannel(signalChannel)
.signalUserCount(signalUserCount)
.signalFurniCount(sourceItem != null ? 1 : 0)
.contextVariableScope(ctx.contextVariables())
.contextVariableScope(ctx.contextVariables().copy())
.triggeredByEffect(true);
if (actor != null) builder.actor(actor);
@@ -286,15 +286,6 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
}
}
if (room != null && room.getRoomSpecialTypes() != null) {
for (HabboItem receiver : newItems) {
int count = room.getRoomSpecialTypes().countSendersTargetingReceiver(receiver.getId(), this);
if (count >= RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER) {
throw new WiredSaveException("Maximum of " + RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER + " senders per receiver reached");
}
}
}
int delay = settings.getDelay();
if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) {
throw new WiredSaveException("Delay too long");
@@ -34,6 +34,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
private static final long DELIVERY_DEDUP_TTL_MS = 60_000L;
private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512;
private static final ConcurrentHashMap<String, Long> DELIVERY_DEDUP = new ConcurrentHashMap<>();
private static final int DEFAULT_SHOW_MESSAGE_MAX_LENGTH = 200;
private static final int DEFAULT_SHOW_MESSAGE_MAX_LINES = 8;
protected String message = "";
protected int userSource = WiredSourceUtil.SOURCE_TRIGGER;
@@ -96,9 +98,12 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) {
message = Emulator.getGameEnvironment().getWordFilter().filter(message, null);
message = message.substring(0, Math.min(message.length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
}
int maxLength = Emulator.getConfig().getInt("hotel.wired.show_message.max_length", DEFAULT_SHOW_MESSAGE_MAX_LENGTH);
int maxLines = Emulator.getConfig().getInt("hotel.wired.show_message.max_lines", DEFAULT_SHOW_MESSAGE_MAX_LINES);
message = clampMessage(message, maxLength, maxLines);
int delay = settings.getDelay();
if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20))
@@ -109,6 +114,35 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
return true;
}
private static String clampMessage(String value, int maxLength, int maxLines) {
if (value == null || value.isEmpty()) {
return "";
}
int safeMaxLength = Math.max(1, maxLength);
int safeMaxLines = Math.max(1, maxLines);
String normalized = value.replace("\r\n", "\n").replace('\r', '\n');
String[] lines = normalized.split("\n", -1);
StringBuilder builder = new StringBuilder();
int linesToWrite = Math.min(lines.length, safeMaxLines);
for (int index = 0; index < linesToWrite; index++) {
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(lines[index]);
}
if (builder.length() > safeMaxLength) {
builder.setLength(safeMaxLength);
}
return builder.toString();
}
protected List<RoomUnit> resolveUsers(WiredContext ctx) {
return WiredSourceUtil.resolveUsers(ctx, this.userSource);
}
@@ -212,7 +246,9 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
}
String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo);
habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.getBubble(this.bubbleStyle))));
habbo.getClient().sendResponse(new RoomUserWhisperComposer(
new RoomChatMessage(msg, habbo.getRoomUnit(), RoomChatMessageBubbles.getBubble(this.bubbleStyle))
));
if (habbo.getRoomUnit().isIdle()) {
habbo.getRoomUnit().getRoom().unIdle(habbo);
@@ -170,13 +170,15 @@ public class WiredExtraTextInputVariable extends InteractionWiredExtra {
}
public Integer resolveCapturedValue(Room room, String rawValue) {
String normalizedValue = rawValue != null ? rawValue.trim() : "";
if (normalizedValue.isEmpty()) {
return null;
}
String capturedValue = rawValue != null ? rawValue : "";
String normalizedValue = capturedValue.trim();
if (this.getDisplayType(room) == DISPLAY_TEXTUAL) {
return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, normalizedValue);
return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, capturedValue);
}
if (normalizedValue.isEmpty()) {
return null;
}
try {
@@ -22,6 +22,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
public static final int CODE = 79;
public static final int MAX_MAPPING_LENGTH = 1000;
public static final int MAX_MAPPING_LINES = 30;
private static final String PRESERVED_SPACE = "\u00A0";
private String mappingsText = "";
private LinkedHashMap<Integer, String> mappings = new LinkedHashMap<>();
@@ -123,8 +124,12 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return "";
}
if (this.mappings.containsKey(value)) {
String mappedValue = this.mappings.get(value);
return mappedValue != null ? mappedValue : String.valueOf(value);
return mappedValue != null ? preserveSpaces(mappedValue) : "";
}
return String.valueOf(value);
}
public Integer resolveValue(String text) {
@@ -132,17 +137,16 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return null;
}
String normalizedText = text.trim();
if (normalizedText.isEmpty()) {
return null;
}
String normalizedText = normalizePreservedSpaces(text);
for (Map.Entry<Integer, String> entry : this.mappings.entrySet()) {
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
continue;
}
if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) {
String normalizedMappingValue = normalizePreservedSpaces(entry.getValue());
if (normalizedMappingValue.equalsIgnoreCase(normalizedText)) {
return entry.getKey();
}
}
@@ -195,8 +199,8 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
continue;
}
String line = rawLine.trim();
if (line.isEmpty()) {
String line = rawLine;
if (line.trim().isEmpty()) {
continue;
}
@@ -210,7 +214,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
}
String keyPart = line.substring(0, separatorIndex).trim();
String valuePart = line.substring(separatorIndex + 1).trim();
String valuePart = line.substring(separatorIndex + 1);
try {
result.put(Integer.parseInt(keyPart), valuePart);
@@ -221,6 +225,14 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return result;
}
private static String preserveSpaces(String value) {
return value.replace(" ", PRESERVED_SPACE);
}
private static String normalizePreservedSpaces(String value) {
return value.replace(PRESERVED_SPACE, " ");
}
static class JsonData {
String mappingsText;
@@ -95,6 +95,11 @@ public class WiredEffectFurniAltitude extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -100,6 +100,11 @@ public class WiredEffectFurniArea extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -155,6 +155,11 @@ public class WiredEffectFurniByType extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -38,6 +38,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false;
@@ -69,8 +70,20 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
int totalRaw = 0;
int wiredSkipped = 0;
Set<HabboItem> result = new LinkedHashSet<>();
Set<HabboItem> neighborhoodItems = new LinkedHashSet<>();
for (int[] src : sourcePositions) {
LOGGER.info("[FurniNeighborhood] Source: ({},{}), offsets: {}", src[0], src[1], tileOffsets.size());
for (int[] offset : getFullGridOffsets()) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
for (HabboItem item : room.getItemsAt(tx, ty)) {
if (item != null && (includeWiredItems || !(item instanceof InteractionWired))) {
neighborhoodItems.add(item);
}
}
}
for (int[] offset : tileOffsets) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
@@ -91,7 +104,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
}
LOGGER.info("[FurniNeighborhood] Raw={}, wiredSkipped={}, kept={}", totalRaw, wiredSkipped, result.size());
result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), filterExisting, invert);
result = this.applyNeighborhoodModifiers(result, neighborhoodItems, ctx.targets().items());
// Always set the selector result even if empty.
// An empty result means no items matched the neighborhood, so downstream
@@ -100,15 +113,51 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
LOGGER.info("[FurniNeighborhood] Set {} items as targets", result.size());
}
private List<int[]> getFullGridOffsets() {
List<int[]> offsets = new ArrayList<>();
for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) {
for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) {
offsets.add(new int[]{ x, y });
}
}
return offsets;
}
private LinkedHashSet<HabboItem> applyNeighborhoodModifiers(Set<HabboItem> matchedTargets,
Set<HabboItem> neighborhoodTargets,
Collection<HabboItem> existingTargets) {
LinkedHashSet<HabboItem> matched = new LinkedHashSet<>(matchedTargets);
if (this.invert) {
LinkedHashSet<HabboItem> base = new LinkedHashSet<>(neighborhoodTargets);
base.removeAll(matched);
if (this.filterExisting) {
base.retainAll(this.toLinkedHashSet(existingTargets));
}
return base;
}
if (this.filterExisting) {
matched.retainAll(this.toLinkedHashSet(existingTargets));
}
return matched;
}
private List<int[]> resolveSourcePositions(WiredContext ctx, Room room) {
switch (sourceType) {
case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) {
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y });
Optional<RoomUnit> actor = ctx.actor();
if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
}
return ctx.actor()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() }))
return ctx.tile()
.map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList());
}
case SOURCE_USER_SIGNAL: {
@@ -260,6 +309,16 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
return true;
}
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasItems();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -128,6 +128,11 @@ public class WiredEffectFurniOnFurni extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -86,6 +86,11 @@ public class WiredEffectFurniPicks extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -77,6 +77,11 @@ public class WiredEffectFurniSignal extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -86,6 +86,11 @@ public class WiredEffectUsersArea extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -92,6 +92,11 @@ public class WiredEffectUsersByAction extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -90,6 +90,11 @@ public class WiredEffectUsersByName extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersByType extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay()));
@@ -90,6 +90,11 @@ public class WiredEffectUsersGroup extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay()));
@@ -73,6 +73,11 @@ public class WiredEffectUsersHandItem extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay()));
@@ -38,6 +38,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false;
@@ -87,11 +88,25 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
LOGGER.debug("[Neighborhood] Target tiles: {}", targetTiles);
Set<String> neighborhoodTiles = new HashSet<>();
for (int[] src : sourcePositions) {
for (int[] offset : getFullGridOffsets()) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
neighborhoodTiles.add(tx + "," + ty);
}
}
List<RoomUnit> result = new ArrayList<>();
List<RoomUnit> neighborhoodUsers = new ArrayList<>();
for (RoomUnit unit : room.getRoomUnits()) {
String pos = unit.getX() + "," + unit.getY();
boolean onTile = targetTiles.contains(pos);
if (neighborhoodTiles.contains(pos)) {
neighborhoodUsers.add(unit);
}
LOGGER.debug("[Neighborhood] Unit id={} type={} pos={} onTile={}", unit.getId(), unit.getRoomUnitType(), pos, onTile);
if (onTile) {
@@ -99,7 +114,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
}
}
result = new ArrayList<>(this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), filterExisting, invert));
result = new ArrayList<>(this.applyNeighborhoodModifiers(result, neighborhoodUsers, ctx.targets().users()));
LOGGER.debug("[Neighborhood] Result: {} users selected", result.size());
@@ -110,15 +125,51 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
ctx.targets().setUsers(result);
}
private List<int[]> getFullGridOffsets() {
List<int[]> offsets = new ArrayList<>();
for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) {
for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) {
offsets.add(new int[]{ x, y });
}
}
return offsets;
}
private LinkedHashSet<RoomUnit> applyNeighborhoodModifiers(Collection<RoomUnit> matchedTargets,
Collection<RoomUnit> neighborhoodTargets,
Collection<RoomUnit> existingTargets) {
LinkedHashSet<RoomUnit> matched = new LinkedHashSet<>(matchedTargets);
if (this.invert) {
LinkedHashSet<RoomUnit> base = new LinkedHashSet<>(neighborhoodTargets);
base.removeAll(matched);
if (this.filterExisting) {
base.retainAll(this.toLinkedHashSet(existingTargets));
}
return base;
}
if (this.filterExisting) {
matched.retainAll(this.toLinkedHashSet(existingTargets));
}
return matched;
}
private List<int[]> resolveSourcePositions(WiredContext ctx, Room room) {
switch (sourceType) {
case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) {
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y });
Optional<RoomUnit> actor = ctx.actor();
if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
}
return ctx.actor()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() }))
return ctx.tile()
.map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList());
}
case SOURCE_USER_SIGNAL: {
@@ -262,6 +313,16 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
return true;
}
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasUsers();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -115,6 +115,11 @@ public class WiredEffectUsersOnFurni extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -71,6 +71,11 @@ public class WiredEffectUsersSignal extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersTeam extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay()));
@@ -187,6 +187,11 @@ public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEf
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refreshReferenceItems();
@@ -370,8 +370,14 @@ public class PetManager {
} else {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
LOGGER.error("Missing petdata for type {}. Adding this to the database...", type);
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type) VALUES (?)")) {
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type, pet_name, offspring_type, happy_actions, tired_actions, random_actions, can_swim) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
statement.setInt(1, type);
statement.setString(2, getFallbackPetName(type));
statement.setInt(3, getFallbackOffspringType(type));
statement.setString(4, "");
statement.setString(5, "");
statement.setString(6, "");
statement.setString(7, "0");
statement.execute();
}
@@ -411,6 +417,42 @@ public class PetManager {
return this.petData.values();
}
private static String getFallbackPetName(int type) {
switch (type) {
case 0:
return "Dog";
case 1:
return "Cat";
case 2:
return "Crocodile";
case 3:
return "Terrier";
case 4:
return "Bear";
case 5:
return "Pig";
default:
return "pet_type_" + type;
}
}
private static int getFallbackOffspringType(int type) {
switch (type) {
case 0:
return 29;
case 1:
return 28;
case 3:
return 25;
case 4:
return 24;
case 5:
return 30;
default:
return -1;
}
}
public Pet createPet(Item item, String name, String race, String color, GameClient client) {
int type = Integer.parseInt(item.getName().toLowerCase().replace("a0 pet", ""));
@@ -1177,7 +1177,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource()
.getConnection(); PreparedStatement statement = connection.prepareStatement(
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ? WHERE id = ?")) {
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) {
statement.setString(1, this.name);
statement.setString(2, this.description);
statement.setString(3, this.password);
@@ -1228,7 +1228,9 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
statement.setString(39, this.hideWired ? "1" : "0");
statement.setString(40, this.allowUnderpass ? "1" : "0");
statement.setString(41, this.youtubeEnabled ? "1" : "0");
statement.setInt(42, this.id);
statement.setString(42, this.buildersClubTrialLocked ? "1" : "0");
statement.setString(43, (this.buildersClubOriginalState != null ? this.buildersClubOriginalState : RoomState.OPEN).name().toLowerCase());
statement.setInt(44, this.id);
statement.executeUpdate();
this.needsUpdate = false;
} catch (SQLException e) {
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.core.DatabaseLoggable;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ISerialize;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.Incoming;
@@ -204,23 +205,14 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable {
message.appendInt(this.getMessage().length());
// Custom prefix data
String prefixText = "";
String prefixColor = "";
String prefixIcon = "";
String prefixEffect = "";
if (this.habbo != null && this.habbo.getInventory() != null && this.habbo.getInventory().getPrefixesComponent() != null) {
com.eu.habbo.habbohotel.users.UserPrefix activePrefix = this.habbo.getInventory().getPrefixesComponent().getActivePrefix();
if (activePrefix != null) {
prefixText = activePrefix.getText();
prefixColor = activePrefix.getColor();
prefixIcon = activePrefix.getIcon();
prefixEffect = activePrefix.getEffect();
}
}
message.appendString(prefixText);
message.appendString(prefixColor);
message.appendString(prefixIcon);
message.appendString(prefixEffect);
UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.empty();
message.appendString(customizationData.prefixText);
message.appendString(customizationData.prefixColor);
message.appendString(customizationData.prefixIcon);
message.appendString(customizationData.prefixEffect);
message.appendString(customizationData.prefixFont);
message.appendString(customizationData.nickIcon);
message.appendString(customizationData.displayOrder);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
@@ -343,18 +343,16 @@ public class RoomSpecialTypes {
* Adds a wired trigger to the room.
* @param trigger The trigger to add
*/
public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 25;
public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 5;
public static final int MAX_SENDERS_PER_RECEIVER = 5;
public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 0;
public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 0;
public static final int MAX_SENDERS_PER_RECEIVER = 0;
public boolean isSignalSenderLimitReached() {
Set<InteractionWiredEffect> existing = this.getSignalSenders();
return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM;
return false;
}
public boolean isSignalReceiverLimitReached() {
Set<InteractionWiredTrigger> existing = this.wiredTriggers.get(WiredTriggerType.RECEIVE_SIGNAL);
return existing != null && existing.size() >= MAX_SIGNAL_RECEIVERS_PER_ROOM;
return false;
}
public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) {
@@ -0,0 +1,469 @@
package com.eu.habbo.habbohotel.translations;
import com.eu.habbo.Emulator;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class GoogleTranslateManager {
private static final Logger LOGGER = LoggerFactory.getLogger(GoogleTranslateManager.class);
private static final int DEFAULT_TIMEOUT_MS = 5000;
private static final long CACHE_TTL_MS = 1000L * 60L * 60L * 6L;
private static final int MAX_TRANSLATION_CACHE_SIZE = 2048;
private static final int MAX_LANGUAGE_CACHE_SIZE = 32;
private static final String FREE_TRANSLATE_ENDPOINT = "https://translate.googleapis.com/translate_a/single";
private static final List<SupportedLanguage> FREE_SUPPORTED_LANGUAGES = buildFreeSupportedLanguages();
private final Map<String, CachedTranslation> translationCache = Collections.synchronizedMap(
new LinkedHashMap<String, CachedTranslation>(128, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, CachedTranslation> eldest) {
return this.size() > MAX_TRANSLATION_CACHE_SIZE;
}
});
private final Map<String, CachedLanguages> languagesCache = Collections.synchronizedMap(
new LinkedHashMap<String, CachedLanguages>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, CachedLanguages> eldest) {
return this.size() > MAX_LANGUAGE_CACHE_SIZE;
}
});
public SupportedLanguagesResponse getSupportedLanguages(String displayLanguage) {
String normalizedDisplayLanguage = normalizeLanguageCode(displayLanguage, "en");
CachedLanguages cachedLanguages = this.languagesCache.get(normalizedDisplayLanguage);
if ((cachedLanguages != null) && !cachedLanguages.isExpired()) {
return SupportedLanguagesResponse.success(new ArrayList<>(cachedLanguages.languages));
}
ArrayList<SupportedLanguage> supportedLanguages = new ArrayList<>(FREE_SUPPORTED_LANGUAGES);
this.languagesCache.put(normalizedDisplayLanguage, new CachedLanguages(supportedLanguages));
return SupportedLanguagesResponse.success(supportedLanguages);
}
public TranslationResponse translate(String text, String targetLanguage) {
String safeText = text == null ? "" : text;
String normalizedTargetLanguage = normalizeLanguageCode(targetLanguage, "en");
if (safeText.trim().isEmpty()) {
return TranslationResponse.success(safeText, safeText, "", normalizedTargetLanguage);
}
String cacheKey = normalizedTargetLanguage + '\u0000' + safeText;
CachedTranslation cachedTranslation = this.translationCache.get(cacheKey);
if ((cachedTranslation != null) && !cachedTranslation.isExpired()) {
return cachedTranslation.response;
}
try {
String requestUrl = FREE_TRANSLATE_ENDPOINT
+ "?client=gtx"
+ "&sl=auto"
+ "&tl=" + encode(normalizedTargetLanguage)
+ "&dt=t"
+ "&q=" + encode(safeText);
HttpsURLConnection connection = this.openGet(requestUrl);
int statusCode = connection.getResponseCode();
if (statusCode != 200) {
return TranslationResponse.failure(safeText, normalizedTargetLanguage, this.readErrorMessage(connection));
}
JsonArray response = this.readJsonArray(connection.getInputStream());
JsonArray translatedParts = response.size() > 0 && response.get(0).isJsonArray()
? response.get(0).getAsJsonArray()
: new JsonArray();
StringBuilder translatedText = new StringBuilder();
for (int index = 0; index < translatedParts.size(); index++) {
if (!translatedParts.get(index).isJsonArray()) {
continue;
}
JsonArray translatedPart = translatedParts.get(index).getAsJsonArray();
if (translatedPart.size() > 0 && !translatedPart.get(0).isJsonNull()) {
translatedText.append(translatedPart.get(0).getAsString());
}
}
String detectedLanguage = "";
if (response.size() > 2 && !response.get(2).isJsonNull()) {
detectedLanguage = response.get(2).getAsString();
}
String resolvedTranslation = translatedText.length() > 0 ? translatedText.toString() : safeText;
TranslationResponse translationResponse = TranslationResponse.success(safeText, resolvedTranslation, detectedLanguage, normalizedTargetLanguage);
this.translationCache.put(cacheKey, new CachedTranslation(translationResponse));
return translationResponse;
} catch (Exception e) {
LOGGER.error("Failed to translate text with Google Translate", e);
return TranslationResponse.failure(safeText, normalizedTargetLanguage, "Failed to translate text with Google Translate.");
}
}
public void clearCache() {
this.translationCache.clear();
this.languagesCache.clear();
}
private int getTimeoutMs() {
return Math.max(1000, Emulator.getConfig().getInt("translate.google.timeout.ms", DEFAULT_TIMEOUT_MS));
}
private HttpsURLConnection openGet(String requestUrl) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) URI.create(requestUrl).toURL().openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(this.getTimeoutMs());
connection.setReadTimeout(this.getTimeoutMs());
connection.setRequestProperty("Accept", "application/json");
return connection;
}
private JsonObject readJson(InputStream inputStream) throws IOException {
try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
return JsonParser.parseReader(bufferedReader).getAsJsonObject();
}
}
private JsonArray readJsonArray(InputStream inputStream) throws IOException {
try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
return JsonParser.parseReader(bufferedReader).getAsJsonArray();
}
}
private String readErrorMessage(HttpsURLConnection connection) {
try {
InputStream errorStream = connection.getErrorStream();
if (errorStream == null) {
return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.';
}
try {
JsonObject errorResponse = this.readJson(errorStream);
if (errorResponse.has("error") && errorResponse.get("error").isJsonObject()) {
JsonObject errorObject = errorResponse.getAsJsonObject("error");
if (errorObject.has("message")) {
return errorObject.get("message").getAsString();
}
}
} catch (Exception ignored) {
try (InputStreamReader inputStreamReader = new InputStreamReader(errorStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
StringBuilder responseText = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
responseText.append(line);
}
if (responseText.length() > 0) {
return responseText.toString();
}
}
}
} catch (Exception e) {
LOGGER.warn("Failed to parse Google Translate error response", e);
}
try {
return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.';
} catch (IOException e) {
return "Google Translate request failed.";
}
}
private static String normalizeLanguageCode(String languageCode, String fallback) {
if (languageCode == null || languageCode.trim().isEmpty()) {
return fallback;
}
String normalized = languageCode.trim().replace('_', '-');
String[] split = normalized.split("-");
if (split.length <= 1) {
return normalized;
}
return split[0] + '-' + split[1].toUpperCase();
}
private static String encode(String value) {
return URLEncoder.encode(value == null ? "" : value, StandardCharsets.UTF_8);
}
private static List<SupportedLanguage> buildFreeSupportedLanguages() {
ArrayList<SupportedLanguage> languages = new ArrayList<>();
addLanguage(languages, "af", "Afrikaans");
addLanguage(languages, "sq", "Albanian");
addLanguage(languages, "am", "Amharic");
addLanguage(languages, "ar", "Arabic");
addLanguage(languages, "hy", "Armenian");
addLanguage(languages, "az", "Azerbaijani");
addLanguage(languages, "eu", "Basque");
addLanguage(languages, "be", "Belarusian");
addLanguage(languages, "bn", "Bengali");
addLanguage(languages, "bs", "Bosnian");
addLanguage(languages, "bg", "Bulgarian");
addLanguage(languages, "ca", "Catalan");
addLanguage(languages, "ceb", "Cebuano");
addLanguage(languages, "ny", "Chichewa");
addLanguage(languages, "zh-CN", "Chinese (Simplified)");
addLanguage(languages, "zh-TW", "Chinese (Traditional)");
addLanguage(languages, "co", "Corsican");
addLanguage(languages, "hr", "Croatian");
addLanguage(languages, "cs", "Czech");
addLanguage(languages, "da", "Danish");
addLanguage(languages, "nl", "Dutch");
addLanguage(languages, "en", "English");
addLanguage(languages, "eo", "Esperanto");
addLanguage(languages, "et", "Estonian");
addLanguage(languages, "tl", "Filipino");
addLanguage(languages, "fi", "Finnish");
addLanguage(languages, "fr", "French");
addLanguage(languages, "fy", "Frisian");
addLanguage(languages, "gl", "Galician");
addLanguage(languages, "ka", "Georgian");
addLanguage(languages, "de", "German");
addLanguage(languages, "el", "Greek");
addLanguage(languages, "gu", "Gujarati");
addLanguage(languages, "ht", "Haitian Creole");
addLanguage(languages, "ha", "Hausa");
addLanguage(languages, "haw", "Hawaiian");
addLanguage(languages, "iw", "Hebrew");
addLanguage(languages, "hi", "Hindi");
addLanguage(languages, "hmn", "Hmong");
addLanguage(languages, "hu", "Hungarian");
addLanguage(languages, "is", "Icelandic");
addLanguage(languages, "ig", "Igbo");
addLanguage(languages, "id", "Indonesian");
addLanguage(languages, "ga", "Irish");
addLanguage(languages, "it", "Italian");
addLanguage(languages, "ja", "Japanese");
addLanguage(languages, "jw", "Javanese");
addLanguage(languages, "kn", "Kannada");
addLanguage(languages, "kk", "Kazakh");
addLanguage(languages, "km", "Khmer");
addLanguage(languages, "rw", "Kinyarwanda");
addLanguage(languages, "ko", "Korean");
addLanguage(languages, "ku", "Kurdish");
addLanguage(languages, "ky", "Kyrgyz");
addLanguage(languages, "lo", "Lao");
addLanguage(languages, "la", "Latin");
addLanguage(languages, "lv", "Latvian");
addLanguage(languages, "lt", "Lithuanian");
addLanguage(languages, "lb", "Luxembourgish");
addLanguage(languages, "mk", "Macedonian");
addLanguage(languages, "mg", "Malagasy");
addLanguage(languages, "ms", "Malay");
addLanguage(languages, "ml", "Malayalam");
addLanguage(languages, "mt", "Maltese");
addLanguage(languages, "mi", "Maori");
addLanguage(languages, "mr", "Marathi");
addLanguage(languages, "mn", "Mongolian");
addLanguage(languages, "my", "Myanmar");
addLanguage(languages, "ne", "Nepali");
addLanguage(languages, "no", "Norwegian");
addLanguage(languages, "or", "Odia");
addLanguage(languages, "ps", "Pashto");
addLanguage(languages, "fa", "Persian");
addLanguage(languages, "pl", "Polish");
addLanguage(languages, "pt", "Portuguese");
addLanguage(languages, "pa", "Punjabi");
addLanguage(languages, "ro", "Romanian");
addLanguage(languages, "ru", "Russian");
addLanguage(languages, "sm", "Samoan");
addLanguage(languages, "gd", "Scots");
addLanguage(languages, "sr", "Serbian");
addLanguage(languages, "st", "Sesotho");
addLanguage(languages, "sn", "Shona");
addLanguage(languages, "sd", "Sindhi");
addLanguage(languages, "si", "Sinhala");
addLanguage(languages, "sk", "Slovak");
addLanguage(languages, "sl", "Slovenian");
addLanguage(languages, "so", "Somali");
addLanguage(languages, "es", "Spanish");
addLanguage(languages, "su", "Sundanese");
addLanguage(languages, "sw", "Swahili");
addLanguage(languages, "sv", "Swedish");
addLanguage(languages, "tg", "Tajik");
addLanguage(languages, "ta", "Tamil");
addLanguage(languages, "tt", "Tatar");
addLanguage(languages, "te", "Telugu");
addLanguage(languages, "th", "Thai");
addLanguage(languages, "tr", "Turkish");
addLanguage(languages, "tk", "Turkmen");
addLanguage(languages, "uk", "Ukrainian");
addLanguage(languages, "ur", "Urdu");
addLanguage(languages, "ug", "Uyghur");
addLanguage(languages, "uz", "Uzbek");
addLanguage(languages, "vi", "Vietnamese");
addLanguage(languages, "cy", "Welsh");
addLanguage(languages, "xh", "Xhosa");
addLanguage(languages, "yi", "Yiddish");
addLanguage(languages, "yo", "Yoruba");
addLanguage(languages, "zu", "Zulu");
languages.sort(Comparator.comparing(SupportedLanguage::getName, String.CASE_INSENSITIVE_ORDER));
return Collections.unmodifiableList(languages);
}
private static void addLanguage(List<SupportedLanguage> languages, String code, String name) {
languages.add(new SupportedLanguage(code, name));
}
public static class SupportedLanguage {
private final String code;
private final String name;
public SupportedLanguage(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return this.code;
}
public String getName() {
return this.name;
}
}
public static class SupportedLanguagesResponse {
private final boolean success;
private final String errorMessage;
private final List<SupportedLanguage> languages;
private SupportedLanguagesResponse(boolean success, String errorMessage, List<SupportedLanguage> languages) {
this.success = success;
this.errorMessage = errorMessage == null ? "" : errorMessage;
this.languages = languages == null ? Collections.emptyList() : languages;
}
public static SupportedLanguagesResponse success(List<SupportedLanguage> languages) {
return new SupportedLanguagesResponse(true, "", languages);
}
public static SupportedLanguagesResponse failure(String errorMessage) {
return new SupportedLanguagesResponse(false, errorMessage, Collections.emptyList());
}
public boolean isSuccess() {
return this.success;
}
public String getErrorMessage() {
return this.errorMessage;
}
public List<SupportedLanguage> getLanguages() {
return this.languages;
}
}
public static class TranslationResponse {
private final boolean success;
private final String errorMessage;
private final String originalText;
private final String translatedText;
private final String detectedLanguage;
private final String targetLanguage;
private TranslationResponse(boolean success, String errorMessage, String originalText, String translatedText, String detectedLanguage, String targetLanguage) {
this.success = success;
this.errorMessage = errorMessage == null ? "" : errorMessage;
this.originalText = originalText == null ? "" : originalText;
this.translatedText = translatedText == null ? "" : translatedText;
this.detectedLanguage = detectedLanguage == null ? "" : detectedLanguage;
this.targetLanguage = targetLanguage == null ? "" : targetLanguage;
}
public static TranslationResponse success(String originalText, String translatedText, String detectedLanguage, String targetLanguage) {
return new TranslationResponse(true, "", originalText, translatedText, detectedLanguage, targetLanguage);
}
public static TranslationResponse failure(String originalText, String targetLanguage, String errorMessage) {
return new TranslationResponse(false, errorMessage, originalText, originalText, "", targetLanguage);
}
public boolean isSuccess() {
return this.success;
}
public String getErrorMessage() {
return this.errorMessage;
}
public String getOriginalText() {
return this.originalText;
}
public String getTranslatedText() {
return this.translatedText;
}
public String getDetectedLanguage() {
return this.detectedLanguage;
}
public String getTargetLanguage() {
return this.targetLanguage;
}
}
private static class CachedTranslation {
private final long createdAt;
private final TranslationResponse response;
private CachedTranslation(TranslationResponse response) {
this.createdAt = System.currentTimeMillis();
this.response = response;
}
private boolean isExpired() {
return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS;
}
}
private static class CachedLanguages {
private final long createdAt;
private final List<SupportedLanguage> languages;
private CachedLanguages(List<SupportedLanguage> languages) {
this.createdAt = System.currentTimeMillis();
this.languages = languages;
}
private boolean isExpired() {
return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS;
}
}
}
@@ -23,6 +23,8 @@ public class HabboInventory {
private ItemsComponent itemsComponent;
private PetsComponent petsComponent;
private PrefixesComponent prefixesComponent;
private NickIconsComponent nickIconsComponent;
private UserVisualSettingsComponent userVisualSettingsComponent;
public HabboInventory(Habbo habbo) {
this.habbo = habbo;
@@ -68,6 +70,18 @@ public class HabboInventory {
LOGGER.error("Caught exception", e);
}
try {
this.nickIconsComponent = new NickIconsComponent(this.habbo);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
try {
this.userVisualSettingsComponent = new UserVisualSettingsComponent(this.habbo);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
this.items = MarketPlace.getOwnOffers(this.habbo);
}
@@ -127,6 +141,22 @@ public class HabboInventory {
this.prefixesComponent = prefixesComponent;
}
public NickIconsComponent getNickIconsComponent() {
return this.nickIconsComponent;
}
public void setNickIconsComponent(NickIconsComponent nickIconsComponent) {
this.nickIconsComponent = nickIconsComponent;
}
public UserVisualSettingsComponent getUserVisualSettingsComponent() {
return this.userVisualSettingsComponent;
}
public void setUserVisualSettingsComponent(UserVisualSettingsComponent userVisualSettingsComponent) {
this.userVisualSettingsComponent = userVisualSettingsComponent;
}
public void dispose() {
this.badgesComponent.dispose();
this.botsComponent.dispose();
@@ -135,6 +165,8 @@ public class HabboInventory {
this.petsComponent.dispose();
this.wardrobeComponent.dispose();
this.prefixesComponent.dispose();
this.nickIconsComponent.dispose();
this.userVisualSettingsComponent.dispose();
this.badgesComponent = null;
this.botsComponent = null;
@@ -143,6 +175,8 @@ public class HabboInventory {
this.petsComponent = null;
this.wardrobeComponent = null;
this.prefixesComponent = null;
this.nickIconsComponent = null;
this.userVisualSettingsComponent = null;
}
public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) {
@@ -0,0 +1,121 @@
package com.eu.habbo.habbohotel.users;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.inventory.UserVisualSettingsComponent;
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 UserCustomizationData {
private static final Logger LOGGER = LoggerFactory.getLogger(UserCustomizationData.class);
public final String nickIcon;
public final String displayOrder;
public final String prefixText;
public final String prefixColor;
public final String prefixIcon;
public final String prefixEffect;
public final String prefixFont;
private UserCustomizationData(String nickIcon, String displayOrder, String prefixText, String prefixColor, String prefixIcon, String prefixEffect, String prefixFont) {
this.nickIcon = nickIcon != null ? nickIcon : "";
this.displayOrder = UserVisualSettingsComponent.sanitizeDisplayOrder(displayOrder);
this.prefixText = prefixText != null ? prefixText : "";
this.prefixColor = prefixColor != null ? prefixColor : "";
this.prefixIcon = prefixIcon != null ? prefixIcon : "";
this.prefixEffect = prefixEffect != null ? prefixEffect : "";
this.prefixFont = prefixFont != null ? prefixFont : "";
}
public static UserCustomizationData fromHabbo(Habbo habbo) {
if (habbo == null) {
return empty();
}
String nickIcon = "";
String displayOrder = UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER;
String prefixText = "";
String prefixColor = "";
String prefixIcon = "";
String prefixEffect = "";
String prefixFont = "";
if (habbo.getInventory() != null) {
if (habbo.getInventory().getNickIconsComponent() != null) {
UserNickIcon activeNickIcon = habbo.getInventory().getNickIconsComponent().getActiveNickIcon();
if (activeNickIcon != null && activeNickIcon.getIconKey() != null) {
nickIcon = activeNickIcon.getIconKey();
}
}
if (habbo.getInventory().getPrefixesComponent() != null) {
UserPrefix activePrefix = habbo.getInventory().getPrefixesComponent().getActivePrefix();
if (activePrefix != null) {
prefixText = activePrefix.getText();
prefixColor = activePrefix.getColor();
prefixIcon = activePrefix.getIcon();
prefixEffect = activePrefix.getEffect();
prefixFont = activePrefix.getFont();
}
}
if (habbo.getInventory().getUserVisualSettingsComponent() != null) {
displayOrder = habbo.getInventory().getUserVisualSettingsComponent().getDisplayOrder();
}
}
return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont);
}
public static UserCustomizationData fromUserId(int userId) {
String nickIcon = "";
String prefixText = "";
String prefixColor = "";
String prefixIcon = "";
String prefixEffect = "";
String prefixFont = "";
String displayOrder = UserVisualSettingsComponent.loadDisplayOrder(userId);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement nickStatement = connection.prepareStatement(
"SELECT icon_key FROM user_nick_icons WHERE user_id = ? AND active = 1 LIMIT 1")) {
nickStatement.setInt(1, userId);
try (ResultSet set = nickStatement.executeQuery()) {
if (set.next()) {
nickIcon = set.getString("icon_key");
}
}
}
try (PreparedStatement prefixStatement = connection.prepareStatement(
"SELECT text, color, icon, effect, font FROM user_prefixes WHERE user_id = ? AND active = 1 LIMIT 1")) {
prefixStatement.setInt(1, userId);
try (ResultSet set = prefixStatement.executeQuery()) {
if (set.next()) {
prefixText = set.getString("text");
prefixColor = set.getString("color");
prefixIcon = set.getString("icon");
prefixEffect = set.getString("effect");
prefixFont = set.getString("font");
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while loading user customization data", e);
}
return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont);
}
public static UserCustomizationData empty() {
return new UserCustomizationData("", UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER, "", "", "", "", "");
}
}
@@ -0,0 +1,118 @@
package com.eu.habbo.habbohotel.users;
import com.eu.habbo.Emulator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
public class UserNickIcon implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(UserNickIcon.class);
private int id;
private final int userId;
private String iconKey;
private boolean active;
private boolean needsInsert;
private boolean needsUpdate;
private boolean needsDelete;
public UserNickIcon(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.userId = set.getInt("user_id");
this.iconKey = set.getString("icon_key");
this.active = set.getBoolean("active");
this.needsInsert = false;
this.needsUpdate = false;
this.needsDelete = false;
}
public UserNickIcon(int userId, String iconKey) {
this.id = 0;
this.userId = userId;
this.iconKey = iconKey;
this.active = false;
this.needsInsert = true;
this.needsUpdate = false;
this.needsDelete = false;
}
@Override
public void run() {
try {
if (this.needsInsert) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user_nick_icons (user_id, icon_key, active) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, this.userId);
statement.setString(2, this.iconKey);
statement.setBoolean(3, this.active);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
this.id = set.getInt(1);
}
}
}
this.needsInsert = false;
} else if (this.needsDelete) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"DELETE FROM user_nick_icons WHERE id = ? AND user_id = ?")) {
statement.setInt(1, this.id);
statement.setInt(2, this.userId);
statement.execute();
}
this.needsDelete = false;
} else if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE user_nick_icons SET icon_key = ?, active = ? WHERE id = ? AND user_id = ?")) {
statement.setString(1, this.iconKey);
statement.setBoolean(2, this.active);
statement.setInt(3, this.id);
statement.setInt(4, this.userId);
statement.execute();
}
this.needsUpdate = false;
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public int getId() {
return this.id;
}
public int getUserId() {
return this.userId;
}
public String getIconKey() {
return this.iconKey;
}
public void setIconKey(String iconKey) {
this.iconKey = iconKey;
this.needsUpdate = true;
}
public boolean isActive() {
return this.active;
}
public void setActive(boolean active) {
this.active = active;
this.needsUpdate = true;
}
public void needsDelete(boolean needsDelete) {
this.needsDelete = needsDelete;
}
}
@@ -15,6 +15,12 @@ public class UserPrefix implements Runnable {
private String color;
private String icon;
private String effect;
private String font;
private int catalogPrefixId;
private String displayName;
private int points;
private int pointsType;
private boolean custom;
private boolean active;
private boolean needsInsert;
private boolean needsUpdate;
@@ -29,6 +35,12 @@ public class UserPrefix implements Runnable {
if (this.icon == null) this.icon = "";
this.effect = set.getString("effect");
if (this.effect == null) this.effect = "";
this.font = readString(set, "font", "");
this.catalogPrefixId = readInt(set, "catalog_prefix_id", 0);
this.displayName = readString(set, "display_name", this.text);
this.points = readInt(set, "points", 0);
this.pointsType = readInt(set, "points_type", 0);
this.custom = readBoolean(set, "is_custom", true);
this.active = set.getBoolean("active");
this.needsInsert = false;
this.needsUpdate = false;
@@ -36,12 +48,22 @@ public class UserPrefix implements Runnable {
}
public UserPrefix(int userId, String text, String color, String icon, String effect) {
this(userId, text, color, icon, effect, "", 0, text, 0, 0, true);
}
public UserPrefix(int userId, String text, String color, String icon, String effect, String font, int catalogPrefixId, String displayName, int points, int pointsType, boolean custom) {
this.id = 0;
this.userId = userId;
this.text = text;
this.color = color;
this.icon = icon != null ? icon : "";
this.effect = effect != null ? effect : "";
this.font = font != null ? font : "";
this.catalogPrefixId = catalogPrefixId;
this.displayName = (displayName != null && !displayName.isEmpty()) ? displayName : text;
this.points = points;
this.pointsType = pointsType;
this.custom = custom;
this.active = false;
this.needsInsert = true;
this.needsUpdate = false;
@@ -54,14 +76,20 @@ public class UserPrefix implements Runnable {
if (this.needsInsert) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user_prefixes (user_id, text, color, icon, effect, active) VALUES (?, ?, ?, ?, ?, ?)",
"INSERT INTO user_prefixes (user_id, text, color, icon, effect, font, active, catalog_prefix_id, display_name, points, points_type, is_custom) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, this.userId);
statement.setString(2, this.text);
statement.setString(3, this.color);
statement.setString(4, this.icon);
statement.setString(5, this.effect);
statement.setBoolean(6, this.active);
statement.setString(6, this.font);
statement.setBoolean(7, this.active);
statement.setInt(8, this.catalogPrefixId);
statement.setString(9, this.displayName);
statement.setInt(10, this.points);
statement.setInt(11, this.pointsType);
statement.setBoolean(12, this.custom);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
@@ -82,14 +110,20 @@ public class UserPrefix implements Runnable {
} else if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, active = ? WHERE id = ? AND user_id = ?")) {
"UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, font = ?, active = ?, catalog_prefix_id = ?, display_name = ?, points = ?, points_type = ?, is_custom = ? WHERE id = ? AND user_id = ?")) {
statement.setString(1, this.text);
statement.setString(2, this.color);
statement.setString(3, this.icon);
statement.setString(4, this.effect);
statement.setBoolean(5, this.active);
statement.setInt(6, this.id);
statement.setInt(7, this.userId);
statement.setString(5, this.font);
statement.setBoolean(6, this.active);
statement.setInt(7, this.catalogPrefixId);
statement.setString(8, this.displayName);
statement.setInt(9, this.points);
statement.setInt(10, this.pointsType);
statement.setBoolean(11, this.custom);
statement.setInt(12, this.id);
statement.setInt(13, this.userId);
statement.execute();
}
this.needsUpdate = false;
@@ -109,6 +143,13 @@ public class UserPrefix implements Runnable {
public void setIcon(String icon) { this.icon = icon != null ? icon : ""; }
public String getEffect() { return this.effect; }
public void setEffect(String effect) { this.effect = effect != null ? effect : ""; }
public String getFont() { return this.font; }
public void setFont(String font) { this.font = font != null ? font : ""; }
public int getCatalogPrefixId() { return this.catalogPrefixId; }
public String getDisplayName() { return this.displayName; }
public int getPoints() { return this.points; }
public int getPointsType() { return this.pointsType; }
public boolean isCustom() { return this.custom; }
public boolean isActive() { return this.active; }
public void setActive(boolean active) {
@@ -119,4 +160,29 @@ public class UserPrefix implements Runnable {
public void needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; }
public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; }
public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; }
private static int readInt(ResultSet set, String columnName, int defaultValue) {
try {
return set.getInt(columnName);
} catch (SQLException e) {
return defaultValue;
}
}
private static String readString(ResultSet set, String columnName, String defaultValue) {
try {
String value = set.getString(columnName);
return value != null ? value : defaultValue;
} catch (SQLException e) {
return defaultValue;
}
}
private static boolean readBoolean(ResultSet set, String columnName, boolean defaultValue) {
try {
return set.getBoolean(columnName);
} catch (SQLException e) {
return defaultValue;
}
}
}
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.users.inventory;
import com.eu.habbo.Emulator;
import com.eu.habbo.database.SqlQueries;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInventory;
@@ -10,6 +9,7 @@ import com.eu.habbo.plugin.events.inventory.InventoryItemAddedEvent;
import com.eu.habbo.plugin.events.inventory.InventoryItemRemovedEvent;
import com.eu.habbo.plugin.events.inventory.InventoryItemsAddedEvent;
import gnu.trove.TCollections;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.THashMap;
import gnu.trove.map.hash.TIntObjectHashMap;
@@ -18,9 +18,11 @@ import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
public class ItemsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(ItemsComponent.class);
@@ -37,23 +39,25 @@ public class ItemsComponent {
public static THashMap<Integer, HabboItem> loadItems(Habbo habbo) {
THashMap<Integer, HabboItem> itemsList = new THashMap<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items.* FROM items LEFT JOIN builders_club_items ON builders_club_items.item_id = items.id WHERE items.room_id = ? AND items.user_id = ? AND builders_club_items.item_id IS NULL")) {
statement.setInt(1, 0);
statement.setInt(2, habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
try {
SqlQueries.forEach(
"SELECT * FROM items WHERE room_id = ? AND user_id = ?",
rs -> {
try {
HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(rs);
HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(set);
if (item != null) {
itemsList.put(rs.getInt("id"), item);
itemsList.put(set.getInt("id"), item);
} else {
LOGGER.error("Failed to load HabboItem: {}", rs.getInt("id"));
LOGGER.error("Failed to load HabboItem: {}", set.getInt("id"));
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
},
0, habbo.getHabboInfo().getId());
} catch (SqlQueries.DataAccessException e) {
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
@@ -147,45 +151,70 @@ public class ItemsComponent {
public void dispose() {
synchronized (this.items) {
TIntObjectIterator<HabboItem> items = this.items.iterator();
if (items == null) {
LOGGER.error("Items is NULL!");
return;
}
if (!this.items.isEmpty()) {
List<HabboItem> updates = new ArrayList<>();
List<HabboItem> deletes = new ArrayList<>();
for (HabboItem item : this.items.valueCollection()) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement updateStmt = connection.prepareStatement(
"UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) {
try (PreparedStatement deleteStmt = connection.prepareStatement(
"DELETE FROM items WHERE id = ?")) {
int updateCount = 0;
int deleteCount = 0;
for (int i = this.items.size(); i-- > 0; ) {
try {
items.advance();
} catch (NoSuchElementException e) {
break;
}
HabboItem item = items.value();
if (item.needsDelete()) {
deletes.add(item);
deleteStmt.setInt(1, item.getId());
deleteStmt.addBatch();
deleteCount++;
item.needsUpdate(false);
item.needsDelete(false);
} else if (item.needsUpdate()) {
updates.add(item);
updateStmt.setInt(1, item.getUserId());
updateStmt.setInt(2, item.getRoomId());
updateStmt.setString(3, item.getWallPosition());
updateStmt.setInt(4, item.getX());
updateStmt.setInt(5, item.getY());
updateStmt.setDouble(6, item.getZ());
updateStmt.setInt(7, item.getRotation());
updateStmt.setString(8, item.getExtradata());
updateStmt.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells());
updateStmt.setInt(10, item.getId());
updateStmt.addBatch();
updateCount++;
item.needsUpdate(false);
}
if (updateCount > 0 && updateCount % 100 == 0) {
updateStmt.executeBatch();
}
if (deleteCount > 0 && deleteCount % 100 == 0) {
deleteStmt.executeBatch();
}
}
try {
if (!deletes.isEmpty()) {
SqlQueries.batchUpdate(
"DELETE FROM items WHERE id = ?",
deletes,
(ps, item) -> ps.setInt(1, item.getId()));
if (deleteCount % 100 != 0) {
deleteStmt.executeBatch();
}
if (!updates.isEmpty()) {
SqlQueries.batchUpdate(
"UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?",
updates,
(ps, item) -> {
ps.setInt(1, item.getUserId());
ps.setInt(2, item.getRoomId());
ps.setString(3, item.getWallPosition());
ps.setInt(4, item.getX());
ps.setInt(5, item.getY());
ps.setDouble(6, item.getZ());
ps.setInt(7, item.getRotation());
ps.setString(8, item.getExtradata());
ps.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells());
ps.setInt(10, item.getId());
});
if (updateCount % 100 != 0) {
updateStmt.executeBatch();
}
} catch (SqlQueries.DataAccessException e) {
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception during batch item save", e);
}
}
@@ -0,0 +1,119 @@
package com.eu.habbo.habbohotel.users.inventory;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserNickIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class NickIconsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(NickIconsComponent.class);
private final List<UserNickIcon> nickIcons = new ArrayList<>();
private final Habbo habbo;
public NickIconsComponent(Habbo habbo) {
this.habbo = habbo;
this.loadNickIcons();
}
private void loadNickIcons() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM user_nick_icons WHERE user_id = ?")) {
statement.setInt(1, this.habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
this.nickIcons.add(new UserNickIcon(set));
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public List<UserNickIcon> getNickIcons() {
synchronized (this.nickIcons) {
return new ArrayList<>(this.nickIcons);
}
}
public UserNickIcon getActiveNickIcon() {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.isActive()) {
return nickIcon;
}
}
}
return null;
}
public UserNickIcon getNickIcon(int id) {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.getId() == id) {
return nickIcon;
}
}
}
return null;
}
public UserNickIcon getNickIconByKey(String iconKey) {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.getIconKey().equalsIgnoreCase(iconKey)) {
return nickIcon;
}
}
}
return null;
}
public void addNickIcon(UserNickIcon nickIcon) {
synchronized (this.nickIcons) {
this.nickIcons.add(nickIcon);
}
}
public void setActive(int nickIconId) {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
boolean shouldBeActive = (nickIcon.getId() == nickIconId);
if (nickIcon.isActive() != shouldBeActive) {
nickIcon.setActive(shouldBeActive);
Emulator.getThreading().run(nickIcon);
}
}
}
}
public void deactivateAll() {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.isActive()) {
nickIcon.setActive(false);
Emulator.getThreading().run(nickIcon);
}
}
}
}
public void dispose() {
synchronized (this.nickIcons) {
this.nickIcons.clear();
}
}
}
@@ -62,6 +62,15 @@ public class PrefixesComponent {
return null;
}
public UserPrefix getPrefixByCatalogId(int catalogPrefixId) {
synchronized (this.prefixes) {
for (UserPrefix prefix : this.prefixes) {
if (prefix.getCatalogPrefixId() == catalogPrefixId) return prefix;
}
}
return null;
}
public void addPrefix(UserPrefix prefix) {
synchronized (this.prefixes) {
this.prefixes.add(prefix);
@@ -0,0 +1,94 @@
package com.eu.habbo.habbohotel.users.inventory;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class UserVisualSettingsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(UserVisualSettingsComponent.class);
public static final String DEFAULT_DISPLAY_ORDER = "icon-prefix-name";
private static final Set<String> ALLOWED_PARTS = new HashSet<>(Arrays.asList("icon", "prefix", "name"));
private final Habbo habbo;
private String displayOrder = DEFAULT_DISPLAY_ORDER;
public UserVisualSettingsComponent(Habbo habbo) {
this.habbo = habbo;
this.loadSettings();
}
private void loadSettings() {
this.displayOrder = loadDisplayOrder(this.habbo.getHabboInfo().getId());
}
public String getDisplayOrder() {
return sanitizeDisplayOrder(this.displayOrder);
}
public void setDisplayOrder(String displayOrder) {
this.displayOrder = sanitizeDisplayOrder(displayOrder);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user_visual_settings (user_id, display_order) VALUES (?, ?) ON DUPLICATE KEY UPDATE display_order = VALUES(display_order)")) {
statement.setInt(1, this.habbo.getHabboInfo().getId());
statement.setString(2, this.displayOrder);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while saving user visual settings", e);
}
}
public static String loadDisplayOrder(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT display_order FROM user_visual_settings WHERE user_id = ? LIMIT 1")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return sanitizeDisplayOrder(set.getString("display_order"));
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while loading user visual settings", e);
}
return DEFAULT_DISPLAY_ORDER;
}
public static String sanitizeDisplayOrder(String displayOrder) {
if (displayOrder == null || displayOrder.trim().isEmpty()) {
return DEFAULT_DISPLAY_ORDER;
}
String[] parts = displayOrder.trim().toLowerCase().split("-");
if (parts.length != 3) {
return DEFAULT_DISPLAY_ORDER;
}
Set<String> uniqueParts = new HashSet<>();
for (String part : parts) {
if (!ALLOWED_PARTS.contains(part) || !uniqueParts.add(part)) {
return DEFAULT_DISPLAY_ORDER;
}
}
return String.join("-", parts);
}
public void dispose() {
this.displayOrder = DEFAULT_DISPLAY_ORDER;
}
}
@@ -77,6 +77,22 @@ public interface IWiredEffect {
return false;
}
/**
* Selectors can use this to gate stack execution after their target list has
* been resolved. Returning false stops the stack before conditions/effects.
*/
default boolean hasRequiredSelectorTargets(WiredContext ctx) {
return true;
}
/**
* Selectors that filter the current selector result should run after
* selectors that create/replace that result.
*/
default boolean usesExistingSelectorTargets() {
return false;
}
/**
* Simulate this effect's execution and record intended state changes.
* <p>
@@ -112,7 +112,7 @@ public final class WiredContext {
this.state = state;
this.legacySettings = legacySettings;
this.contextVariables = (event.getContextVariableScope() != null)
? event.getContextVariableScope()
? event.getContextVariableScope().copy()
: new WiredContextVariableScope();
this.targets = new WiredTargets();
@@ -26,6 +26,7 @@ import com.eu.habbo.habbohotel.wired.api.WiredStack;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent;
import gnu.trove.map.hash.THashMap;
@@ -130,6 +131,9 @@ public final class WiredEngine {
/** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey;
/** Track filter-selector animation tokens so rapid executions do not reset newer animations */
private final ConcurrentHashMap<Integer, Long> filteredSelectorAnimationTokens;
/**
* Create a new wired engine.
*
@@ -151,6 +155,7 @@ public final class WiredEngine {
this.bannedRooms = new ConcurrentHashMap<>();
this.roomDiagnostics = new ConcurrentHashMap<>();
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
this.filteredSelectorAnimationTokens = new ConcurrentHashMap<>();
}
/**
@@ -426,6 +431,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -541,6 +550,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -627,6 +640,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
return !executableEffects.isEmpty();
@@ -660,6 +677,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false);
if (!conditionsPassedForExecution) {
return false;
@@ -1011,9 +1032,27 @@ public final class WiredEngine {
if (effects.isEmpty()) return Collections.emptyList();
List<InteractionWiredEffect> executedSelectors = new ArrayList<>();
List<IWiredEffect> immediateSelectors = new ArrayList<>();
List<IWiredEffect> deferredSelectors = new ArrayList<>();
for (IWiredEffect effect : effects) {
if (!effect.isSelector()) continue;
if (effect.usesExistingSelectorTargets()) {
deferredSelectors.add(effect);
} else {
immediateSelectors.add(effect);
}
}
executeSelectorList(immediateSelectors, ctx, executedSelectors);
executeSelectorList(deferredSelectors, ctx, executedSelectors);
return executedSelectors;
}
private void executeSelectorList(List<IWiredEffect> selectors, WiredContext ctx, List<InteractionWiredEffect> executedSelectors) {
for (IWiredEffect effect : selectors) {
if (effect.requiresActor() && !ctx.hasActor()) {
continue;
}
@@ -1022,14 +1061,17 @@ public final class WiredEngine {
try {
effect.execute(ctx);
if (effect instanceof InteractionWiredEffect) {
executedSelectors.add((InteractionWiredEffect) effect);
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
executedSelectors.add(wiredEffect);
if (wiredEffect.usesExistingSelectorTargets()) {
setFilteredSelectorState(ctx.room(), wiredEffect, "3");
}
}
} catch (Exception e) {
LOGGER.warn("Error executing selector: {}", e.getMessage());
}
}
return executedSelectors;
}
private void finalizeSelectors(List<InteractionWiredEffect> executedSelectors, WiredContext ctx, long currentTime) {
@@ -1042,9 +1084,58 @@ public final class WiredEngine {
for (InteractionWiredEffect wiredEffect : executedSelectors) {
wiredEffect.setCooldown(currentTime);
if (wiredEffect.usesExistingSelectorTargets()) {
animateFilteredSelectorBox(room, wiredEffect);
} else {
wiredEffect.activateBox(room, actor, currentTime);
}
}
}
private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) {
if (room == null || wiredEffect == null || room.isHideWired()) {
return;
}
long animationToken = System.nanoTime();
this.filteredSelectorAnimationTokens.put(wiredEffect.getId(), animationToken);
setFilteredSelectorState(room, wiredEffect, "3", animationToken, false);
scheduleFilteredSelectorState(room, wiredEffect, "4", animationToken, 80L, false);
scheduleFilteredSelectorState(room, wiredEffect, "5", animationToken, 160L, false);
scheduleFilteredSelectorState(room, wiredEffect, "3", animationToken, 240L, true);
}
private void scheduleFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, long delay, boolean clearToken) {
Emulator.getThreading().run(() -> setFilteredSelectorState(room, wiredEffect, state, animationToken, clearToken), delay);
}
private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state) {
setFilteredSelectorState(room, wiredEffect, state, 0L, false);
}
private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, boolean clearToken) {
if (room == null || wiredEffect == null || room.isHideWired()) {
return;
}
if (animationToken != 0L) {
Long currentToken = this.filteredSelectorAnimationTokens.get(wiredEffect.getId());
if (currentToken == null || currentToken != animationToken) {
return;
}
}
if (!state.equals(wiredEffect.getExtradata())) {
wiredEffect.setExtradata(state);
room.sendComposer(new ItemStateComposer(wiredEffect).compose());
}
if (clearToken) {
this.filteredSelectorAnimationTokens.remove(wiredEffect.getId(), animationToken);
}
}
private void applySelectionFilterExtras(WiredStack stack, WiredContext ctx, List<InteractionWiredEffect> executedSelectors) {
if (executedSelectors == null || executedSelectors.isEmpty()) {
@@ -1059,6 +1150,20 @@ public final class WiredEngine {
WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx);
}
private boolean selectorsHaveRequiredTargets(List<InteractionWiredEffect> executedSelectors, WiredContext ctx) {
if (executedSelectors == null || executedSelectors.isEmpty()) {
return true;
}
for (InteractionWiredEffect selector : executedSelectors) {
if (!selector.hasRequiredSelectorTargets(ctx)) {
return false;
}
}
return true;
}
/**
* Schedule a delayed effect execution.
*/
@@ -151,8 +151,20 @@ public final class WiredSourceUtil {
selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems());
List<InteractionWiredEffect> selectorEffects = getOrderedSelectorEffects(originalCtx, room, triggerItem);
executeSelectorEffects(selectorCtx, selectorEffects, false);
executeSelectorEffects(selectorCtx, selectorEffects, true);
applySelectionFilterExtras(room, triggerItem, selectorCtx);
return selectorCtx;
}
private static void executeSelectorEffects(WiredContext selectorCtx, List<InteractionWiredEffect> selectorEffects, boolean deferred) {
for (InteractionWiredEffect effect : selectorEffects) {
if (effect == null || effect.usesExistingSelectorTargets() != deferred) {
continue;
}
if (effect.requiresActor() && !selectorCtx.hasActor()) {
continue;
}
@@ -163,10 +175,6 @@ public final class WiredSourceUtil {
} catch (Exception ignored) {
}
}
applySelectionFilterExtras(room, triggerItem, selectorCtx);
return selectorCtx;
}
private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) {
@@ -64,8 +64,17 @@ public final class WiredTextInputCaptureSupport {
return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch();
}
MatchResult matchResult = matchTemplate(trigger, text, capturersByName);
MatchResult matchResult = matchTemplate(trigger, text, capturersByName, room);
if (!matchResult.matches) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] NO_MATCH room={} triggerId={} mode={} key='{}' text='{}' len={}",
room.getId(),
stack.triggerItem().getId(),
trigger.getMatchMode(),
safeForLog(trigger.getKey()),
safeForLog(text),
(text != null ? text.length() : 0));
}
return CaptureResult.noMatch();
}
@@ -78,12 +87,28 @@ public final class WiredTextInputCaptureSupport {
Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue());
if (resolvedValue == null) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] RESOLVE_FAIL room={} triggerId={} capturer='{}' raw='{}' rawLen={}",
room.getId(),
stack.triggerItem().getId(),
capture.getKey(),
safeForLog(capture.getValue()),
(capture.getValue() != null ? capture.getValue().length() : 0));
}
return CaptureResult.noMatch();
}
capturedValues.put(capturer.getVariableItemId(), resolvedValue);
}
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] MATCH_OK room={} triggerId={} captures={} textLen={}",
room.getId(),
stack.triggerItem().getId(),
capturedValues.size(),
(text != null ? text.length() : 0));
}
return CaptureResult.matched(capturedValues);
}
@@ -108,12 +133,13 @@ public final class WiredTextInputCaptureSupport {
return capturers;
}
private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName) {
String text = rawText != null ? rawText.trim() : "";
private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName, Room room) {
String text = rawText != null ? rawText : "";
String normalizedText = text.trim();
String template = trigger.getKey() != null ? trigger.getKey().trim() : "";
if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) {
if (capturersByName.size() != 1 || text.isEmpty()) {
if (capturersByName.size() != 1 || normalizedText.isEmpty()) {
return MatchResult.noMatch();
}
@@ -123,12 +149,24 @@ public final class WiredTextInputCaptureSupport {
return MatchResult.matched(captures);
}
MatchResult adjacentCaptureResult = matchAdjacentCapturers(template, rawText, capturersByName, room, trigger.getMatchMode());
if (adjacentCaptureResult != null) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] ADJACENT mode used key='{}' textLen={} matched={}",
safeForLog(template),
(rawText != null ? rawText.length() : 0),
adjacentCaptureResult.matches);
}
return adjacentCaptureResult;
}
TemplatePattern pattern = buildPattern(template);
if (pattern == null) {
return MatchResult.noMatch();
}
Matcher matcher = pattern.pattern.matcher(text);
String matchText = pattern.placeholderNames.isEmpty() ? normalizedText : text;
Matcher matcher = pattern.pattern.matcher(matchText);
boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches();
if (!matches) {
return MatchResult.noMatch();
@@ -142,12 +180,136 @@ public final class WiredTextInputCaptureSupport {
}
String capturedValue = matcher.group(index + 1);
captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : "");
captures.put(placeholderName, normalizeCapturedValue(capturedValue));
}
return MatchResult.matched(captures);
}
private static MatchResult matchAdjacentCapturers(String template, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName, Room room, int matchMode) {
if (template == null || template.isEmpty() || rawText == null || capturersByName == null || capturersByName.isEmpty() || room == null) {
return null;
}
Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
List<String> placeholderNames = new ArrayList<>();
int cursor = 0;
while (matcher.find()) {
if (matcher.start() != cursor) {
return null;
}
String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : "";
if (placeholderName.isEmpty() || !capturersByName.containsKey(placeholderName)) {
return null;
}
placeholderNames.add(placeholderName);
cursor = matcher.end();
}
if (placeholderNames.isEmpty() || cursor != template.length()) {
return null;
}
int placeholderCount = placeholderNames.size();
int textLength = rawText.length();
boolean[][] reachable = new boolean[placeholderCount + 1][textLength + 1];
int[][] previousIndex = new int[placeholderCount + 1][textLength + 1];
String[][] capturedValues = new String[placeholderCount + 1][textLength + 1];
for (int placeholderIndex = 0; placeholderIndex <= placeholderCount; placeholderIndex++) {
for (int textIndex = 0; textIndex <= textLength; textIndex++) {
previousIndex[placeholderIndex][textIndex] = -1;
}
}
reachable[0][0] = true;
for (int placeholderIndex = 0; placeholderIndex < placeholderCount; placeholderIndex++) {
String placeholderName = placeholderNames.get(placeholderIndex);
WiredExtraTextInputVariable capturer = capturersByName.get(placeholderName);
if (capturer == null) {
return MatchResult.noMatch();
}
for (int textIndex = 0; textIndex <= textLength; textIndex++) {
if (!reachable[placeholderIndex][textIndex]) {
continue;
}
int minEndIndex = (textIndex < textLength) ? (textIndex + 1) : textIndex;
for (int endIndex = minEndIndex; endIndex <= textLength; endIndex++) {
if (reachable[placeholderIndex + 1][endIndex]) {
continue;
}
String candidate = rawText.substring(textIndex, endIndex);
if (capturer.resolveCapturedValue(room, candidate) == null) {
continue;
}
reachable[placeholderIndex + 1][endIndex] = true;
previousIndex[placeholderIndex + 1][endIndex] = textIndex;
capturedValues[placeholderIndex + 1][endIndex] = candidate;
}
}
}
int resultEndIndex = -1;
if (matchMode == MATCH_CONTAINS) {
for (int endIndex = textLength; endIndex >= 0; endIndex--) {
if (reachable[placeholderCount][endIndex]) {
resultEndIndex = endIndex;
break;
}
}
} else if (reachable[placeholderCount][textLength]) {
resultEndIndex = textLength;
}
if (resultEndIndex < 0) {
return MatchResult.noMatch();
}
LinkedHashMap<String, String> captures = new LinkedHashMap<>();
int backtrackTextIndex = resultEndIndex;
for (int placeholderIndex = placeholderCount; placeholderIndex > 0; placeholderIndex--) {
String placeholderName = placeholderNames.get(placeholderIndex - 1);
String capturedValue = capturedValues[placeholderIndex][backtrackTextIndex];
captures.put(placeholderName, capturedValue != null ? capturedValue : "");
backtrackTextIndex = previousIndex[placeholderIndex][backtrackTextIndex];
if (backtrackTextIndex < 0) {
return MatchResult.noMatch();
}
}
return MatchResult.matched(captures);
}
private static String normalizeCapturedValue(String value) {
return value != null ? value : "";
}
private static String safeForLog(String value) {
if (value == null) {
return "";
}
String normalized = value
.replace("\r", "\\r")
.replace("\n", "\\n")
.replace("\u00A0", "");
if (normalized.length() > 180) {
return normalized.substring(0, 180) + "...(" + normalized.length() + ")";
}
return normalized;
}
private static TemplatePattern buildPattern(String template) {
if (template == null || template.isEmpty()) {
return null;
@@ -160,7 +322,7 @@ public final class WiredTextInputCaptureSupport {
while (matcher.find()) {
regex.append(Pattern.quote(template.substring(cursor, matcher.start())));
regex.append("(.+?)");
regex.append(hasPlaceholderAfter(template, matcher.end()) ? "(.+?)" : "(.+)");
String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : "";
placeholderNames.add(placeholderName);
@@ -176,6 +338,10 @@ public final class WiredTextInputCaptureSupport {
return new TemplatePattern(Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE), placeholderNames);
}
private static boolean hasPlaceholderAfter(String template, int cursor) {
return PLACEHOLDER_PATTERN.matcher(template.substring(cursor)).find();
}
public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) {
if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) {
return;
@@ -32,6 +32,8 @@ import java.util.List;
import java.util.Locale;
public final class WiredTextPlaceholderUtil {
private static final char PRESERVED_SPACE = '\u00A0';
private WiredTextPlaceholderUtil() {
}
@@ -87,7 +89,41 @@ public final class WiredTextPlaceholderUtil {
}
}
return resolvedText;
return preserveRepeatedSpaces(resolvedText);
}
private static String preserveRepeatedSpaces(String text) {
if (text == null || text.length() < 2) {
return text;
}
StringBuilder result = new StringBuilder(text.length());
int index = 0;
while (index < text.length()) {
char currentChar = text.charAt(index);
if (currentChar != ' ') {
result.append(currentChar);
index++;
continue;
}
int startIndex = index;
while (index < text.length() && text.charAt(index) == ' ') {
index++;
}
int spaceCount = index - startIndex;
if (spaceCount == 1) {
result.append(' ');
continue;
}
for (int spaceIndex = 0; spaceIndex < spaceCount; spaceIndex++) {
result.append(PRESERVED_SPACE);
}
}
return result.toString();
}
public static boolean requiresActor(Room room, HabboItem stackItem) {
@@ -275,7 +311,7 @@ public final class WiredTextPlaceholderUtil {
}
String value = resolveRoomVariableValue(room, extra);
return (value == null || value.isEmpty()) ? List.of() : List.of(value);
return value == null ? List.of() : List.of(value);
}
private static List<String> collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) {
@@ -284,7 +320,7 @@ public final class WiredTextPlaceholderUtil {
}
String value = resolveContextVariableValue(ctx, extra);
return (value == null || value.isEmpty()) ? List.of() : List.of(value);
return value == null ? List.of() : List.of(value);
}
private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) {
@@ -11,6 +11,8 @@ import java.util.List;
import java.util.Map;
public final class WiredVariableTextConnectorSupport {
private static final String PRESERVED_SPACE = "\u00A0";
private WiredVariableTextConnectorSupport() {
}
@@ -71,7 +73,7 @@ public final class WiredVariableTextConnectorSupport {
Map<Integer, String> mappings = connector.getMappings();
if (mappings.containsKey(value)) {
String mappedValue = mappings.get(value);
return mappedValue != null ? mappedValue : String.valueOf(value);
return mappedValue != null ? preserveSpaces(mappedValue) : "";
}
}
@@ -83,10 +85,7 @@ public final class WiredVariableTextConnectorSupport {
return null;
}
String normalizedText = text.trim();
if (normalizedText.isEmpty()) {
return null;
}
String normalizedText = normalizePreservedSpaces(text);
for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) {
Integer mappedValue = connector.resolveValue(normalizedText);
@@ -97,4 +96,12 @@ public final class WiredVariableTextConnectorSupport {
return null;
}
private static String preserveSpaces(String value) {
return value.replace(" ", PRESERVED_SPACE);
}
private static String normalizePreservedSpaces(String value) {
return value.replace(PRESERVED_SPACE, " ");
}
}
@@ -36,6 +36,7 @@ import com.eu.habbo.messages.incoming.helper.MySanctionStatusEvent;
import com.eu.habbo.messages.incoming.helper.RequestTalentTrackEvent;
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.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*;
@@ -61,6 +62,8 @@ import com.eu.habbo.messages.incoming.rooms.promotions.RequestPromotionRoomsEven
import com.eu.habbo.messages.incoming.rooms.promotions.UpdateRoomPromotionEvent;
import com.eu.habbo.messages.incoming.rooms.users.*;
import com.eu.habbo.messages.incoming.trading.*;
import com.eu.habbo.messages.incoming.translation.TranslationLanguagesRequestEvent;
import com.eu.habbo.messages.incoming.translation.TranslationTextRequestEvent;
import com.eu.habbo.messages.incoming.unknown.RequestResolutionEvent;
import com.eu.habbo.messages.incoming.unknown.UnknownEvent1;
import com.eu.habbo.messages.incoming.users.*;
@@ -117,6 +120,7 @@ public class PacketManager {
this.registerGuilds();
this.registerPets();
this.registerWired();
this.registerTranslation();
this.registerAchievements();
this.registerFloorPlanEditor();
this.registerAmbassadors();
@@ -409,6 +413,13 @@ public class PacketManager {
this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class);
this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class);
this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.class);
this.registerHandler(Incoming.PurchaseCatalogPrefixEvent, PurchaseCatalogPrefixEvent.class);
this.registerHandler(Incoming.SetDisplayOrderEvent, SetDisplayOrderEvent.class);
// Nick Icons
this.registerHandler(Incoming.RequestUserNickIconsEvent, RequestUserNickIconsEvent.class);
this.registerHandler(Incoming.PurchaseNickIconEvent, PurchaseNickIconEvent.class);
this.registerHandler(Incoming.SetActiveNickIconEvent, SetActiveNickIconEvent.class);
}
void registerRooms() throws Exception {
@@ -635,6 +646,11 @@ public class PacketManager {
this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class);
}
void registerTranslation() throws Exception {
this.registerHandler(Incoming.TranslationLanguagesRequestEvent, TranslationLanguagesRequestEvent.class);
this.registerHandler(Incoming.TranslationTextRequestEvent, TranslationTextRequestEvent.class);
}
void registerUnknown() throws Exception {
this.registerHandler(Incoming.RequestResolutionEvent, RequestResolutionEvent.class);
this.registerHandler(Incoming.RequestTalenTrackEvent, RequestTalentTrackEvent.class);
@@ -419,6 +419,8 @@ public class Incoming {
public static final int WiredUserVariableUpdateEvent = 10025;
public static final int WiredUserVariableManageEvent = 10026;
public static final int WiredUserInspectMoveEvent = 10027;
public static final int TranslationLanguagesRequestEvent = 10032;
public static final int TranslationTextRequestEvent = 10033;
public static final int RequestInventoryPetDelete = 10030;
public static final int RequestInventoryBadgeDelete = 10031;
@@ -448,6 +450,11 @@ public class Incoming {
public static final int SetActivePrefixEvent = 7012;
public static final int DeletePrefixEvent = 7013;
public static final int PurchasePrefixEvent = 7014;
public static final int RequestUserNickIconsEvent = 7015;
public static final int PurchaseNickIconEvent = 7016;
public static final int SetActiveNickIconEvent = 7017;
public static final int PurchaseCatalogPrefixEvent = 7018;
public static final int SetDisplayOrderEvent = 7019;
// YouTube Room Broadcast
public static final int YouTubeRoomPlayEvent = 8001;
@@ -0,0 +1,95 @@
package com.eu.habbo.messages.incoming.inventory.nickicons;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserNickIcon;
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.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer;
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 PurchaseNickIconEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PurchaseNickIconEvent.class);
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) {
return;
}
String requestedIconKey = normalizeIconKey(this.packet.readString());
if (requestedIconKey.isEmpty()) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid nick icon selected."));
return;
}
if (habbo.getInventory().getNickIconsComponent().getNickIconByKey(requestedIconKey) != null) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "You already own this nick icon."));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT points, points_type, enabled FROM custom_nick_icons_catalog WHERE icon_key = ? LIMIT 1")) {
statement.setString(1, requestedIconKey);
try (ResultSet set = statement.executeQuery()) {
if (!set.next() || !set.getBoolean("enabled")) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "This nick icon is not available."));
return;
}
int points = set.getInt("points");
int pointsType = set.getInt("points_type");
if (points > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < points) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
return;
}
if (points > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -points);
this.client.sendResponse(new UserCurrencyComposer(habbo));
}
UserNickIcon nickIcon = new UserNickIcon(habbo.getHabboInfo().getId(), requestedIconKey);
nickIcon.run();
habbo.getInventory().getNickIconsComponent().addNickIcon(nickIcon);
this.client.sendResponse(new UserNickIconsComposer(habbo));
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Unable to purchase this nick icon right now."));
}
}
private String normalizeIconKey(String iconKey) {
if (iconKey == null) {
return "";
}
String normalized = iconKey.trim().toLowerCase();
if (normalized.endsWith(".gif")) {
normalized = normalized.substring(0, normalized.length() - 4);
}
return normalized.matches("^[a-z0-9_-]+$") ? normalized : "";
}
}
@@ -0,0 +1,11 @@
package com.eu.habbo.messages.incoming.inventory.nickicons;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
public class RequestUserNickIconsEvent extends MessageHandler {
@Override
public void handle() throws Exception {
this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo()));
}
}
@@ -0,0 +1,34 @@
package com.eu.habbo.messages.incoming.inventory.nickicons;
import com.eu.habbo.habbohotel.users.UserNickIcon;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
public class SetActiveNickIconEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int nickIconId = this.packet.readInt();
if (nickIconId == 0) {
this.client.getHabbo().getInventory().getNickIconsComponent().deactivateAll();
this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo()));
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
}
return;
}
UserNickIcon nickIcon = this.client.getHabbo().getInventory().getNickIconsComponent().getNickIcon(nickIconId);
if (nickIcon == null) {
return;
}
this.client.getHabbo().getInventory().getNickIconsComponent().setActive(nickIconId);
this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo()));
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
}
}
}
@@ -0,0 +1,84 @@
package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.Emulator;
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.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.users.UserCurrencyComposer;
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 PurchaseCatalogPrefixEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PurchaseCatalogPrefixEvent.class);
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
int catalogPrefixId = this.packet.readInt();
Habbo habbo = this.client.getHabbo();
if (habbo == null || catalogPrefixId <= 0) {
return;
}
if (habbo.getInventory().getPrefixesComponent().getPrefixByCatalogId(catalogPrefixId) != null) {
this.client.sendResponse(new UserNickIconsComposer(habbo));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT display_name, text, color, icon, effect, font, points, points_type FROM custom_prefixes_catalog WHERE id = ? AND enabled = 1 LIMIT 1")) {
statement.setInt(1, catalogPrefixId);
try (ResultSet set = statement.executeQuery()) {
if (!set.next()) {
return;
}
int points = set.getInt("points");
int pointsType = set.getInt("points_type");
if (points > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < points) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
return;
}
if (points > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -points);
this.client.sendResponse(new UserCurrencyComposer(habbo));
}
UserPrefix prefix = new UserPrefix(
habbo.getHabboInfo().getId(),
set.getString("text"),
set.getString("color"),
set.getString("icon"),
set.getString("effect"),
set.getString("font"),
catalogPrefixId,
set.getString("display_name"),
points,
pointsType,
false);
prefix.run();
habbo.getInventory().getPrefixesComponent().addPrefix(prefix);
this.client.sendResponse(new UserNickIconsComposer(habbo));
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while purchasing catalog prefix", e);
}
}
}
@@ -6,6 +6,7 @@ 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.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer;
import com.eu.habbo.messages.outgoing.users.UserCreditsComposer;
import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer;
@@ -19,6 +20,7 @@ import java.sql.SQLException;
public class PurchasePrefixEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class);
private static final String[] ALLOWED_FONTS = { "", "pixel", "cherry", "vampiro" };
@Override
public int getRatelimit() {
@@ -31,6 +33,7 @@ public class PurchasePrefixEvent extends MessageHandler {
String color = this.packet.readString();
String icon = this.packet.readString();
String effect = this.packet.readString();
String font = this.packet.readString();
Habbo habbo = this.client.getHabbo();
@@ -42,6 +45,9 @@ public class PurchasePrefixEvent extends MessageHandler {
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);
// Validate text
text = text.trim();
@@ -72,43 +78,67 @@ public class PurchasePrefixEvent extends MessageHandler {
return;
}
if (icon == null) icon = "";
icon = icon.trim();
if (effect == null) effect = "";
effect = effect.trim();
if (font == null) font = "";
font = font.trim().toLowerCase();
if (!isAllowedFont(font)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid font format."));
return;
}
int totalPriceCredits = priceCredits + (!font.isEmpty() ? fontPriceCredits : 0);
// Check credits
if (priceCredits > 0 && habbo.getHabboInfo().getCredits() < priceCredits) {
if (totalPriceCredits > 0 && habbo.getHabboInfo().getCredits() < totalPriceCredits) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits."));
return;
}
int totalPricePointsSameType = pricePoints + ((fontPricePoints > 0 && fontPointsType == pointsType && !font.isEmpty()) ? fontPricePoints : 0);
// Check points
if (pricePoints > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < pricePoints) {
if (totalPricePointsSameType > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < totalPricePointsSameType) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "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."));
return;
}
// Deduct currency
if (priceCredits > 0) {
habbo.getHabboInfo().addCredits(-priceCredits);
if (totalPriceCredits > 0) {
habbo.getHabboInfo().addCredits(-totalPriceCredits);
this.client.sendResponse(new UserCreditsComposer(habbo));
}
if (pricePoints > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -pricePoints);
if (totalPricePointsSameType > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -totalPricePointsSameType);
this.client.sendResponse(new UserCurrencyComposer(habbo));
}
// Validate icon (allow empty or known icon names)
if (icon == null) icon = "";
icon = icon.trim();
// Validate effect
if (effect == null) effect = "";
effect = effect.trim();
if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType) {
habbo.getHabboInfo().addCurrencyAmount(fontPointsType, -fontPricePoints);
this.client.sendResponse(new UserCurrencyComposer(habbo));
}
// Create prefix
UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect);
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);
this.client.sendResponse(new PrefixReceivedComposer(prefix));
this.client.sendResponse(new UserNickIconsComposer(habbo));
}
private int getSettingInt(String key, int defaultValue) {
@@ -142,4 +172,14 @@ public class PurchasePrefixEvent extends MessageHandler {
}
return false;
}
private boolean isAllowedFont(String font) {
for (String allowedFont : ALLOWED_FONTS) {
if (allowedFont.equals(font)) {
return true;
}
}
return false;
}
}
@@ -3,6 +3,8 @@ 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.rooms.users.RoomUserDataComposer;
public class SetActivePrefixEvent extends MessageHandler {
@Override
@@ -12,6 +14,11 @@ public class SetActivePrefixEvent extends MessageHandler {
if (prefixId == 0) {
this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll();
this.client.sendResponse(new ActivePrefixUpdatedComposer(null));
this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo()));
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
}
return;
}
@@ -21,5 +28,10 @@ public class SetActivePrefixEvent extends MessageHandler {
this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId);
this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix));
this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo()));
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
}
}
}
@@ -0,0 +1,26 @@
package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.inventory.UserVisualSettingsComponent;
import com.eu.habbo.messages.incoming.MessageHandler;
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 void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null || habbo.getInventory() == null || habbo.getInventory().getUserVisualSettingsComponent() == null) {
return;
}
String displayOrder = UserVisualSettingsComponent.sanitizeDisplayOrder(this.packet.readString());
habbo.getInventory().getUserVisualSettingsComponent().setDisplayOrder(displayOrder);
this.client.sendResponse(new UserNickIconsComposer(habbo));
if (habbo.getHabboInfo().getCurrentRoom() != null) {
habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
}
}
}
@@ -0,0 +1,28 @@
package com.eu.habbo.messages.incoming.translation;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.translations.GoogleTranslateManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.translation.TranslationLanguagesComposer;
public class TranslationLanguagesRequestEvent extends MessageHandler {
@Override
public void handle() {
final GameClient client = this.client;
final String displayLanguage = this.packet.readString();
Emulator.getThreading().run(() -> {
GoogleTranslateManager.SupportedLanguagesResponse response = Emulator.getGameEnvironment()
.getGoogleTranslateManager()
.getSupportedLanguages(displayLanguage);
client.sendResponse(new TranslationLanguagesComposer(response).compose());
});
}
@Override
public int getRatelimit() {
return 250;
}
}
@@ -0,0 +1,25 @@
package com.eu.habbo.messages.incoming.translation;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.translations.GoogleTranslateManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.translation.TranslationResultComposer;
public class TranslationTextRequestEvent extends MessageHandler {
@Override
public void handle() {
final GameClient client = this.client;
final int requestId = this.packet.readInt();
final String text = this.packet.readString();
final String targetLanguage = this.packet.readString();
Emulator.getThreading().run(() -> {
GoogleTranslateManager.TranslationResponse response = Emulator.getGameEnvironment()
.getGoogleTranslateManager()
.translate(text, targetLanguage);
client.sendResponse(new TranslationResultComposer(requestId, response).compose());
});
}
}
@@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer;
import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer;
import com.eu.habbo.messages.outgoing.wired.WiredSavedComposer;
public class WiredEffectSaveDataEvent extends MessageHandler {
@@ -39,6 +40,16 @@ public class WiredEffectSaveDataEvent extends MessageHandler {
if (saved) {
this.client.sendResponse(new WiredSavedComposer());
if (effect != null) {
if (effect.isSelector()) {
if (effect.usesExistingSelectorTargets()) {
effect.setExtradata("3");
room.sendComposer(new ItemStateComposer(effect).compose());
} else if ("3".equals(effect.getExtradata()) || "4".equals(effect.getExtradata()) || "5".equals(effect.getExtradata())) {
effect.setExtradata("0");
room.sendComposer(new ItemStateComposer(effect).compose());
}
}
effect.needsUpdate(true);
Emulator.getThreading().run(effect);
} else {
@@ -124,6 +124,8 @@ public class Outgoing {
public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM
public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM
public final static int ConfInvisStateComposer = 5104; // CUSTOM
public final static int TranslationLanguagesComposer = 5106; // CUSTOM
public final static int TranslationResultComposer = 5107; // CUSTOM
public final static int AreaHideComposer = 6001; // CUSTOM
public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768
public final static int MarketplaceConfigComposer = 1823; // PRODUCTION-201611291003-338511768
@@ -576,6 +578,7 @@ public class Outgoing {
public static final int UserPrefixesComposer = 7001;
public static final int PrefixReceivedComposer = 7002;
public static final int ActivePrefixUpdatedComposer = 7003;
public static final int UserNickIconsComposer = 7004;
public static final int AvailableCommandsComposer = 4050;
// YouTube Room Broadcast
@@ -0,0 +1,217 @@
package com.eu.habbo.messages.outgoing.inventory.nickicons;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.habbohotel.users.UserNickIcon;
import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class UserNickIconsComposer extends MessageComposer {
private static final Logger LOGGER = LoggerFactory.getLogger(UserNickIconsComposer.class);
private final Habbo habbo;
public UserNickIconsComposer(Habbo habbo) {
this.habbo = habbo;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.UserNickIconsComposer);
if (this.habbo == null || this.habbo.getInventory() == null || this.habbo.getInventory().getNickIconsComponent() == null) {
this.response.appendInt(0);
return this.response;
}
Map<String, UserNickIcon> ownedByKey = new HashMap<>();
List<UserNickIcon> ownedNickIcons = this.habbo.getInventory().getNickIconsComponent().getNickIcons();
for (UserNickIcon nickIcon : ownedNickIcons) {
ownedByKey.put(nickIcon.getIconKey().toLowerCase(), nickIcon);
}
Map<Integer, UserPrefix> ownedPrefixesByCatalogId = new HashMap<>();
List<UserPrefix> ownedPrefixes = this.habbo.getInventory().getPrefixesComponent().getPrefixes();
for (UserPrefix prefix : ownedPrefixes) {
if (prefix.getCatalogPrefixId() > 0) {
ownedPrefixesByCatalogId.put(prefix.getCatalogPrefixId(), prefix);
}
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT icon_key, display_name, points, points_type FROM custom_nick_icons_catalog WHERE enabled = 1 ORDER BY sort_order ASC, id ASC")) {
try (ResultSet set = statement.executeQuery()) {
List<CatalogNickIcon> catalogNickIcons = new ArrayList<>();
while (set.next()) {
catalogNickIcons.add(new CatalogNickIcon(
set.getString("icon_key"),
set.getString("display_name"),
set.getInt("points"),
set.getInt("points_type")));
}
this.response.appendInt(catalogNickIcons.size());
for (CatalogNickIcon catalogNickIcon : catalogNickIcons) {
UserNickIcon ownedNickIcon = ownedByKey.get(catalogNickIcon.iconKey.toLowerCase());
this.response.appendString(catalogNickIcon.iconKey);
this.response.appendString(catalogNickIcon.displayName != null ? catalogNickIcon.displayName : "");
this.response.appendInt(catalogNickIcon.points);
this.response.appendInt(catalogNickIcon.pointsType);
this.response.appendInt(ownedNickIcon != null ? 1 : 0);
this.response.appendInt((ownedNickIcon != null && ownedNickIcon.isActive()) ? 1 : 0);
this.response.appendInt(ownedNickIcon != null ? ownedNickIcon.getId() : 0);
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
this.response.appendInt(0);
}
UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo);
this.response.appendString(customizationData.displayOrder);
this.response.appendInt(this.getSettingInt("max_length", 15));
this.response.appendInt(this.getSettingInt("price_credits", 5));
this.response.appendInt(this.getSettingInt("price_points", 0));
this.response.appendInt(this.getSettingInt("points_type", 0));
this.response.appendInt(this.getSettingInt("font_price_credits", 10));
this.response.appendInt(this.getSettingInt("font_price_points", 0));
this.response.appendInt(this.getSettingInt("font_points_type", 0));
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT id, display_name, text, color, icon, effect, font, points, points_type FROM custom_prefixes_catalog WHERE enabled = 1 ORDER BY sort_order ASC, id ASC")) {
try (ResultSet set = statement.executeQuery()) {
List<CatalogPrefix> catalogPrefixes = new ArrayList<>();
while (set.next()) {
catalogPrefixes.add(new CatalogPrefix(
set.getInt("id"),
set.getString("display_name"),
set.getString("text"),
set.getString("color"),
set.getString("icon"),
set.getString("effect"),
set.getString("font"),
set.getInt("points"),
set.getInt("points_type")));
}
this.response.appendInt(catalogPrefixes.size());
for (CatalogPrefix catalogPrefix : catalogPrefixes) {
UserPrefix ownedPrefix = ownedPrefixesByCatalogId.get(catalogPrefix.id);
this.response.appendInt(catalogPrefix.id);
this.response.appendString(catalogPrefix.displayName != null ? catalogPrefix.displayName : catalogPrefix.text);
this.response.appendString(catalogPrefix.text != null ? catalogPrefix.text : "");
this.response.appendString(catalogPrefix.color != null ? catalogPrefix.color : "");
this.response.appendString(catalogPrefix.icon != null ? catalogPrefix.icon : "");
this.response.appendString(catalogPrefix.effect != null ? catalogPrefix.effect : "");
this.response.appendString(catalogPrefix.font != null ? catalogPrefix.font : "");
this.response.appendInt(catalogPrefix.points);
this.response.appendInt(catalogPrefix.pointsType);
this.response.appendInt(ownedPrefix != null ? 1 : 0);
this.response.appendInt((ownedPrefix != null && ownedPrefix.isActive()) ? 1 : 0);
this.response.appendInt(ownedPrefix != null ? ownedPrefix.getId() : 0);
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception loading prefix catalog", e);
this.response.appendInt(0);
}
this.response.appendInt(ownedPrefixes.size());
for (UserPrefix prefix : ownedPrefixes) {
this.response.appendInt(prefix.getId());
this.response.appendString(prefix.getDisplayName() != null ? prefix.getDisplayName() : prefix.getText());
this.response.appendString(prefix.getText());
this.response.appendString(prefix.getColor());
this.response.appendString(prefix.getIcon());
this.response.appendString(prefix.getEffect());
this.response.appendString(prefix.getFont());
this.response.appendInt(prefix.isActive() ? 1 : 0);
this.response.appendInt(prefix.isCustom() ? 1 : 0);
this.response.appendInt(prefix.getPoints());
this.response.appendInt(prefix.getPointsType());
this.response.appendInt(prefix.getCatalogPrefixId());
}
return this.response;
}
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 = ? LIMIT 1")) {
statement.setString(1, key);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return Integer.parseInt(set.getString("value"));
}
}
} catch (SQLException | NumberFormatException e) {
LOGGER.error("Caught exception while resolving prefix setting {}", key, e);
}
return defaultValue;
}
private static class CatalogNickIcon {
private final String iconKey;
private final String displayName;
private final int points;
private final int pointsType;
private CatalogNickIcon(String iconKey, String displayName, int points, int pointsType) {
this.iconKey = iconKey;
this.displayName = displayName;
this.points = points;
this.pointsType = pointsType;
}
}
private static class CatalogPrefix {
private final int id;
private final String displayName;
private final String text;
private final String color;
private final String icon;
private final String effect;
private final String font;
private final int points;
private final int pointsType;
private CatalogPrefix(int id, String displayName, String text, String color, String icon, String effect, String font, int points, int pointsType) {
this.id = id;
this.displayName = displayName;
this.text = text;
this.color = color;
this.icon = icon;
this.effect = effect;
this.font = font;
this.points = points;
this.pointsType = pointsType;
}
}
}
@@ -22,12 +22,14 @@ public class ActivePrefixUpdatedComposer extends MessageComposer {
this.response.appendString(this.prefix.getColor());
this.response.appendString(this.prefix.getIcon());
this.response.appendString(this.prefix.getEffect());
this.response.appendString(this.prefix.getFont());
} else {
this.response.appendInt(0);
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
}
return this.response;
@@ -20,6 +20,7 @@ public class PrefixReceivedComposer extends MessageComposer {
this.response.appendString(this.prefix.getColor());
this.response.appendString(this.prefix.getIcon());
this.response.appendString(this.prefix.getEffect());
this.response.appendString(this.prefix.getFont());
return this.response;
}
}
@@ -30,6 +30,7 @@ public class UserPrefixesComposer extends MessageComposer {
this.response.appendString(prefix.getColor());
this.response.appendString(prefix.getIcon());
this.response.appendString(prefix.getEffect());
this.response.appendString(prefix.getFont());
this.response.appendInt(prefix.isActive() ? 1 : 0);
}
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.outgoing.rooms.users;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
@@ -24,6 +25,14 @@ public class RoomUserDataComposer extends MessageComposer {
this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg());
UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo);
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
return this.response;
}
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.bots.Bot;
import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
@@ -67,6 +68,14 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString("");
this.response.appendInt(this.habbo.getHabboStats().getAchievementScore());
this.response.appendBoolean(true);
UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo);
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
this.response.appendString(this.habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId());
} else if (this.habbos != null) {
@@ -101,6 +110,14 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString("");
this.response.appendInt(habbo.getHabboStats().getAchievementScore());
this.response.appendBoolean(true);
UserCustomizationData customizationData = UserCustomizationData.fromHabbo(habbo);
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
this.response.appendString(habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId());
}
@@ -0,0 +1,33 @@
package com.eu.habbo.messages.outgoing.translation;
import com.eu.habbo.habbohotel.translations.GoogleTranslateManager;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class TranslationLanguagesComposer extends MessageComposer {
private final GoogleTranslateManager.SupportedLanguagesResponse responseData;
public TranslationLanguagesComposer(GoogleTranslateManager.SupportedLanguagesResponse responseData) {
this.responseData = responseData;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.TranslationLanguagesComposer);
this.response.appendBoolean(this.responseData != null && this.responseData.isSuccess());
this.response.appendString(this.responseData != null ? this.responseData.getErrorMessage() : "Unknown error");
int count = (this.responseData != null) ? this.responseData.getLanguages().size() : 0;
this.response.appendInt(count);
if (this.responseData != null) {
for (GoogleTranslateManager.SupportedLanguage language : this.responseData.getLanguages()) {
this.response.appendString(language.getCode());
this.response.appendString(language.getName());
}
}
return this.response;
}
}
@@ -0,0 +1,29 @@
package com.eu.habbo.messages.outgoing.translation;
import com.eu.habbo.habbohotel.translations.GoogleTranslateManager;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class TranslationResultComposer extends MessageComposer {
private final int requestId;
private final GoogleTranslateManager.TranslationResponse responseData;
public TranslationResultComposer(int requestId, GoogleTranslateManager.TranslationResponse responseData) {
this.requestId = requestId;
this.responseData = responseData;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.TranslationResultComposer);
this.response.appendInt(this.requestId);
this.response.appendBoolean(this.responseData != null && this.responseData.isSuccess());
this.response.appendString(this.responseData != null ? this.responseData.getErrorMessage() : "Unknown error");
this.response.appendString(this.responseData != null ? this.responseData.getOriginalText() : "");
this.response.appendString(this.responseData != null ? this.responseData.getTranslatedText() : "");
this.response.appendString(this.responseData != null ? this.responseData.getDetectedLanguage() : "");
this.response.appendString(this.responseData != null ? this.responseData.getTargetLanguage() : "");
return this.response;
}
}
@@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.messenger.Messenger;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
@@ -116,6 +117,14 @@ public class UserProfileComposer extends MessageComposer {
this.response.appendInt(this.habboInfo.getInfostandStand());
this.response.appendInt(this.habboInfo.getInfostandOverlay());
this.response.appendInt(this.habboInfo.getInfostandCardBg());
UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.fromUserId(this.habboInfo.getId());
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
return this.response;
}
@@ -3,6 +3,8 @@ package com.eu.habbo.networking.gameserver;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.PacketManager;
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler;
import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler;
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
@@ -53,6 +55,8 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("httpCodec", new HttpServerCodec());
ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_FRAME_SIZE));
ch.pipeline().addLast("wsHttpHandler", new WebSocketHttpHandler());
ch.pipeline().addLast("nitroSecureAssetHandler", new NitroSecureAssetHandler());
ch.pipeline().addLast("nitroSecureApiHandler", new NitroSecureApiHandler());
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
@@ -348,6 +348,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
ok.addProperty("username", rot.username);
ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt);
ok.addProperty("rememberExpiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
@@ -410,6 +411,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
JsonObject ok = new JsonObject();
ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt);
ok.addProperty("rememberExpiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
@@ -423,7 +425,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String username = readString(body, "username").trim();
String password = readString(body, "password");
boolean rememberMe = readBoolean(body, "remember", false);
boolean rememberMe = readBoolean(body, "remember", false) || readBoolean(body, "rememberMe", false);
if (username.isEmpty() || password.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials."));
@@ -1137,7 +1139,8 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
}
private static boolean isKeepAlive(FullHttpRequest req) {
@@ -0,0 +1,290 @@
package com.eu.habbo.networking.gameserver.auth;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.*;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public class NitroSecureApiHandler extends ChannelDuplexHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class);
private static final String ENABLED_CONFIG = "nitro.secure.api.enabled";
private static final String API_PREFIX = "/api/";
private static final AttributeKey<Deque<SecureApiContext>> SECURE_CONTEXTS =
AttributeKey.valueOf("nitroSecureApiContexts");
private static final ConcurrentHashMap<String, Long> NONCE_CACHE = new ConcurrentHashMap<>();
private static final long MAX_REQUEST_SKEW_MS = 90_000L;
private static final long NONCE_TTL_MS = 2 * 60 * 1000L;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof FullHttpRequest req)) {
super.channelRead(ctx, msg);
return;
}
String path = new QueryStringDecoder(req.uri()).path();
if (!secureApiEnabled()) {
super.channelRead(ctx, msg);
return;
}
if (!path.startsWith(API_PREFIX)) {
super.channelRead(ctx, msg);
return;
}
if (req.method() == HttpMethod.OPTIONS) {
sendCors(ctx, req);
ReferenceCountUtil.release(req);
return;
}
if (!isSecureRequest(req)) {
super.channelRead(ctx, msg);
return;
}
try {
String clientKey = req.headers().get("X-Nitro-Key");
if (clientKey == null || clientKey.isBlank()) {
sendText(ctx, req, HttpResponseStatus.UNAUTHORIZED, "Missing key.");
return;
}
SecretKey sessionKey = NitroSecureAssetHandler.deriveSessionKey(java.util.Base64.getDecoder().decode(clientKey));
SecureApiContext secureContext = new SecureApiContext(
NitroSecureAssetHandler.getServerKeyFingerprint(),
NitroSecureAssetHandler.fingerprint(sessionKey.getEncoded()),
sessionKey
);
if (!req.content().isReadable()) {
enqueueContext(ctx, secureContext);
super.channelRead(ctx, msg);
return;
}
byte[] encrypted = new byte[req.content().readableBytes()];
req.content().getBytes(req.content().readerIndex(), encrypted);
byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8)));
clear = unwrapEnvelope(clear, req, secureContext);
FullHttpRequest decryptedReq = new DefaultFullHttpRequest(
req.protocolVersion(),
req.method(),
req.uri(),
Unpooled.wrappedBuffer(clear)
);
decryptedReq.headers().setAll(req.headers());
decryptedReq.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
decryptedReq.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, clear.length);
enqueueContext(ctx, secureContext);
ReferenceCountUtil.release(req);
ctx.fireChannelRead(decryptedReq);
} catch (IllegalArgumentException e) {
LOGGER.warn("Nitro secure API rejected invalid encrypted payload", e);
sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, e.getMessage());
ReferenceCountUtil.release(req);
} catch (Exception e) {
LOGGER.error("Nitro secure API failed to decrypt request", e);
sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid secure payload.");
ReferenceCountUtil.release(req);
}
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof FullHttpResponse response)) {
super.write(ctx, msg, promise);
return;
}
SecureApiContext secureContext = pollContext(ctx);
if (secureContext == null) {
super.write(ctx, msg, promise);
return;
}
try {
byte[] clear = readBytes(response.content());
byte[] encrypted = NitroSecureAssetHandler.encrypt(secureContext.sessionKey(), clear);
byte[] hex = NitroSecureAssetHandler.toHex(encrypted).getBytes(StandardCharsets.UTF_8);
FullHttpResponse encryptedResponse = new DefaultFullHttpResponse(
response.protocolVersion(),
response.status(),
Unpooled.wrappedBuffer(hex)
);
encryptedResponse.headers().setAll(response.headers());
encryptedResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8");
encryptedResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, hex.length);
encryptedResponse.headers().set("X-Nitro-Sec", "1");
encryptedResponse.headers().set("X-Nitro-Key-Fp", secureContext.serverKeyFingerprint());
encryptedResponse.headers().set("X-Nitro-Derive-Fp", secureContext.derivedFingerprint());
encryptedResponse.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
ReferenceCountUtil.release(response);
super.write(ctx, encryptedResponse, promise);
} catch (Exception e) {
LOGGER.error("Nitro secure API failed to encrypt response", e);
super.write(ctx, msg, promise);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Deque<SecureApiContext> contexts = ctx.channel().attr(SECURE_CONTEXTS).get();
if (contexts != null) contexts.clear();
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Deque<SecureApiContext> contexts = ctx.channel().attr(SECURE_CONTEXTS).get();
if (contexts != null) contexts.clear();
super.exceptionCaught(ctx, cause);
}
private static boolean isSecureRequest(FullHttpRequest req) {
return "1".equals(req.headers().get("X-Nitro-Api"));
}
private static boolean secureApiEnabled() {
return com.eu.habbo.Emulator.getConfig().getBoolean(ENABLED_CONFIG, true);
}
private static byte[] unwrapEnvelope(byte[] clear, FullHttpRequest req, SecureApiContext secureContext) {
if (!requiresReplayEnvelope(req.method())) return clear;
JsonObject envelope = JsonParser.parseString(new String(clear, StandardCharsets.UTF_8)).getAsJsonObject();
long ts = envelope.has("ts") ? envelope.get("ts").getAsLong() : 0L;
String nonce = envelope.has("nonce") ? envelope.get("nonce").getAsString() : "";
String method = envelope.has("method") ? envelope.get("method").getAsString() : "";
String path = envelope.has("path") ? envelope.get("path").getAsString() : "";
String body = envelope.has("body") ? envelope.get("body").getAsString() : "";
long now = System.currentTimeMillis();
if (Math.abs(now - ts) > MAX_REQUEST_SKEW_MS) {
throw new IllegalArgumentException("Secure request expired.");
}
if (!req.method().name().equalsIgnoreCase(method)) {
throw new IllegalArgumentException("Secure request method mismatch.");
}
String requestPath = req.uri();
if (!requestPath.equals(path)) {
throw new IllegalArgumentException("Secure request path mismatch.");
}
if (nonce.isBlank()) {
throw new IllegalArgumentException("Missing secure request nonce.");
}
cleanupExpiredNonces(now);
String replayKey = secureContext.derivedFingerprint() + ':' + nonce;
if (NONCE_CACHE.putIfAbsent(replayKey, now + NONCE_TTL_MS) != null) {
throw new IllegalArgumentException("Secure request replay detected.");
}
return java.util.Base64.getDecoder().decode(body);
}
private static boolean requiresReplayEnvelope(HttpMethod method) {
return method == HttpMethod.POST
|| method == HttpMethod.PUT
|| method == HttpMethod.PATCH
|| method == HttpMethod.DELETE;
}
private static void cleanupExpiredNonces(long now) {
if (NONCE_CACHE.size() < 512) return;
NONCE_CACHE.entrySet().removeIf(entry -> entry.getValue() < now);
}
private static void enqueueContext(ChannelHandlerContext ctx, SecureApiContext context) {
Deque<SecureApiContext> queue = ctx.channel().attr(SECURE_CONTEXTS).get();
if (queue == null) {
queue = new ArrayDeque<>();
ctx.channel().attr(SECURE_CONTEXTS).set(queue);
}
queue.addLast(context);
}
private static SecureApiContext pollContext(ChannelHandlerContext ctx) {
Deque<SecureApiContext> queue = ctx.channel().attr(SECURE_CONTEXTS).get();
if (queue == null || queue.isEmpty()) return null;
return queue.pollFirst();
}
private static byte[] readBytes(ByteBuf content) {
byte[] bytes = new byte[content.readableBytes()];
content.getBytes(content.readerIndex(), bytes);
return bytes;
}
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
applyCors(req, response);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text) {
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
applyCors(req, response);
boolean keepAlive = isKeepAlive(req);
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
var future = ctx.writeAndFlush(response);
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
}
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
if (origin != null && !origin.isEmpty()) {
response.headers().set("Access-Control-Allow-Origin", origin);
response.headers().set("Vary", "Origin");
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
}
private static boolean isKeepAlive(FullHttpRequest req) {
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
return connection == null || !"close".equalsIgnoreCase(connection);
}
private record SecureApiContext(String serverKeyFingerprint, String derivedFingerprint, SecretKey sessionKey) {
private SecureApiContext {
Objects.requireNonNull(serverKeyFingerprint);
Objects.requireNonNull(derivedFingerprint);
Objects.requireNonNull(sessionKey);
}
}
}
@@ -0,0 +1,355 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureAssetHandler.class);
private static final String MASTER_KEY_CONFIG = "nitro.secure.master_key";
private static final String ENABLED_CONFIG = "nitro.secure.assets.enabled";
private static final String BOOTSTRAP_PATH = "/nitro-sec/bootstrap";
private static final String FILE_PATH = "/nitro-sec/file";
private static final int MAX_BOOTSTRAP_BODY_BYTES = 4096;
private static final SecureRandom RNG = new SecureRandom();
private static final KeyPair SERVER_KEYPAIR = createServerKeyPair();
private static final String SERVER_KEY_FINGERPRINT = fingerprint(SERVER_KEYPAIR.getPublic().getEncoded());
private static final Map<String, CacheEntry> CACHE = new ConcurrentHashMap<>();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof FullHttpRequest req)) {
super.channelRead(ctx, msg);
return;
}
String path = new QueryStringDecoder(req.uri()).path();
if (!secureAssetsEnabled()) {
super.channelRead(ctx, msg);
return;
}
if (!path.equals(BOOTSTRAP_PATH) && !path.equals(FILE_PATH)) {
super.channelRead(ctx, msg);
return;
}
try {
if (req.method() == HttpMethod.OPTIONS) {
sendCors(ctx, req);
return;
}
if (path.equals(BOOTSTRAP_PATH)) handleBootstrap(ctx, req);
else handleFile(ctx, req);
} finally {
ReferenceCountUtil.release(req);
}
}
private void handleBootstrap(ChannelHandlerContext ctx, FullHttpRequest req) {
if (req.method() != HttpMethod.POST) {
sendText(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "Use POST.", "text/plain; charset=utf-8");
return;
}
if (req.content().readableBytes() > MAX_BOOTSTRAP_BODY_BYTES) {
sendText(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, "Payload too large.", "text/plain; charset=utf-8");
return;
}
try {
JsonObject body = JsonParser.parseString(req.content().toString(StandardCharsets.UTF_8)).getAsJsonObject();
String clientKey = body.has("key") ? body.get("key").getAsString() : "";
if (clientKey.isEmpty()) {
sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Missing key.", "text/plain; charset=utf-8");
return;
}
JsonObject response = new JsonObject();
response.addProperty("key", Base64.getEncoder().encodeToString(SERVER_KEYPAIR.getPublic().getEncoded()));
sendText(ctx, req, HttpResponseStatus.OK, response.toString(), "application/json; charset=utf-8");
} catch (Exception e) {
LOGGER.warn("Nitro secure bootstrap failed", e);
sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid bootstrap.", "text/plain; charset=utf-8");
}
}
private void handleFile(ChannelHandlerContext ctx, FullHttpRequest req) {
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendText(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "Use GET.", "text/plain; charset=utf-8");
return;
}
QueryStringDecoder query = new QueryStringDecoder(req.uri());
String clientKey = headerOrQuery(req, query, "X-Nitro-Key", "key");
if (clientKey == null || clientKey.isEmpty()) {
sendText(ctx, req, HttpResponseStatus.UNAUTHORIZED, "Missing key.", "text/plain; charset=utf-8");
return;
}
String kind = queryParam(query, "kind");
String file = queryParam(query, "file");
if (!kind.equals("config") && !kind.equals("gamedata")) {
sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid kind.", "text/plain; charset=utf-8");
return;
}
try {
SecretKey sessionKey = deriveSessionKey(Base64.getDecoder().decode(clientKey));
byte[] clear = readAsset(kind, file);
byte[] encrypted = encrypt(sessionKey, clear);
sendText(ctx, req, HttpResponseStatus.OK, toHex(encrypted), "text/plain; charset=utf-8", true, fingerprint(sessionKey.getEncoded()));
} catch (IllegalArgumentException e) {
sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, e.getMessage(), "text/plain; charset=utf-8");
} catch (IOException e) {
sendText(ctx, req, HttpResponseStatus.NOT_FOUND, "Not found.", "text/plain; charset=utf-8");
} catch (Exception e) {
LOGGER.error("Nitro secure asset failed kind=" + kind + " file=" + file, e);
sendText(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, "Server error.", "text/plain; charset=utf-8");
}
}
private static byte[] readAsset(String kind, String file) throws IOException {
String normalized = normalizeFile(file);
String rootConfigKey = kind.equals("config") ? "nitro.secure.config.root" : "nitro.secure.gamedata.root";
String fallback = kind.equals("config") ? "Nitro-V3/public" : "Nitro-V3/public/nitro/gamedata";
Path root = resolveRoot(rootConfigKey, fallback, kind.equals("config")
? new String[] { "../Nitro-V3/public", "../../Nitro-V3/public", "Nitro-V3/public" }
: new String[] { "../Nitro-V3/public/nitro/gamedata", "../../Nitro-V3/public/nitro/gamedata", "Nitro-V3/public/nitro/gamedata" });
Path target = root.resolve(normalized).normalize();
if (!target.startsWith(root)) throw new IllegalArgumentException("Invalid file.");
if (!Files.isRegularFile(target)) throw new IOException("Not found");
String cacheKey = kind + ":" + target;
long modified = Files.getLastModifiedTime(target).toMillis();
CacheEntry cached = CACHE.get(cacheKey);
if (cached != null && cached.modified == modified) return cached.bytes;
byte[] bytes = Files.readAllBytes(target);
if (normalized.toLowerCase().endsWith(".json")) bytes = minifyJson(bytes);
CACHE.put(cacheKey, new CacheEntry(modified, bytes));
return bytes;
}
private static String normalizeFile(String file) {
if (file == null) throw new IllegalArgumentException("Missing file.");
String value = URLDecoder.decode(file, StandardCharsets.UTF_8).replace('\\', '/');
int queryIndex = value.indexOf('?');
if (queryIndex >= 0) value = value.substring(0, queryIndex);
int fragmentIndex = value.indexOf('#');
if (fragmentIndex >= 0) value = value.substring(0, fragmentIndex);
while (value.startsWith("/")) value = value.substring(1);
if (value.isEmpty() || value.contains("..") || value.contains(":")) throw new IllegalArgumentException("Invalid file.");
return value;
}
private static byte[] minifyJson(byte[] bytes) {
try {
return JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).toString().getBytes(StandardCharsets.UTF_8);
} catch (Exception ignored) {
return bytes;
}
}
private static Path resolveRoot(String configKey, String fallback, String[] alternatives) {
String configured = Emulator.getConfig().getValue(configKey, "");
if (configured != null && !configured.isEmpty()) return Path.of(configured).toAbsolutePath().normalize();
for (String alternative : alternatives) {
Path path = Path.of(alternative).toAbsolutePath().normalize();
if (Files.isDirectory(path)) return path;
}
return Path.of(fallback).toAbsolutePath().normalize();
}
private static boolean secureAssetsEnabled() {
return Emulator.getConfig().getBoolean(ENABLED_CONFIG, true);
}
static SecretKey deriveSessionKey(byte[] clientPublicEncoded) throws Exception {
KeyFactory factory = KeyFactory.getInstance("EC");
PublicKey clientPublic = factory.generatePublic(new X509EncodedKeySpec(clientPublicEncoded));
KeyAgreement agreement = KeyAgreement.getInstance("ECDH");
agreement.init(SERVER_KEYPAIR.getPrivate());
agreement.doPhase(clientPublic, true);
byte[] secret = agreement.generateSecret();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(secret);
digest.update("nitro-secure-assets-v1".getBytes(StandardCharsets.UTF_8));
return new SecretKeySpec(digest.digest(), "AES");
}
static byte[] encrypt(SecretKey key, byte[] clear) throws Exception {
byte[] iv = new byte[12];
RNG.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] encrypted = cipher.doFinal(clear);
byte[] out = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, out, 0, iv.length);
System.arraycopy(encrypted, 0, out, iv.length, encrypted.length);
return out;
}
static byte[] decrypt(SecretKey key, byte[] encryptedPayload) throws Exception {
if (encryptedPayload.length < 13) throw new IllegalArgumentException("Encrypted payload is too short.");
byte[] iv = new byte[12];
byte[] payload = new byte[encryptedPayload.length - iv.length];
System.arraycopy(encryptedPayload, 0, iv, 0, iv.length);
System.arraycopy(encryptedPayload, iv.length, payload, 0, payload.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
return cipher.doFinal(payload);
}
private static KeyPair createServerKeyPair() {
try {
String configuredSecret = Emulator.getConfig().getValue(MASTER_KEY_CONFIG, "");
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
if (configuredSecret != null && !configuredSecret.isBlank()) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] seed = digest.digest(configuredSecret.getBytes(StandardCharsets.UTF_8));
SecureRandom deterministic = SecureRandom.getInstance("SHA1PRNG");
deterministic.setSeed(seed);
generator.initialize(256, deterministic);
LOGGER.info("Nitro secure assets using persistent server key from config {}", MASTER_KEY_CONFIG);
} else {
generator.initialize(256, RNG);
LOGGER.warn("Nitro secure assets using ephemeral server key because {} is empty", MASTER_KEY_CONFIG);
}
return generator.generateKeyPair();
} catch (Exception e) {
throw new IllegalStateException("Unable to create Nitro secure server key", e);
}
}
private static String headerOrQuery(FullHttpRequest req, QueryStringDecoder query, String header, String param) {
String value = req.headers().get(header);
return (value == null || value.isEmpty()) ? queryParam(query, param) : value;
}
private static String queryParam(QueryStringDecoder query, String key) {
if (!query.parameters().containsKey(key) || query.parameters().get(key).isEmpty()) return "";
return query.parameters().get(key).get(0);
}
private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text, String contentType) {
sendBytes(ctx, req, status, text.getBytes(StandardCharsets.UTF_8), contentType, false, null);
}
private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text, String contentType, boolean encrypted, String deriveFingerprint) {
sendBytes(ctx, req, status, text.getBytes(StandardCharsets.UTF_8), contentType, encrypted, deriveFingerprint);
}
private static void sendBytes(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, byte[] bytes, String contentType, boolean encrypted) {
sendBytes(ctx, req, status, bytes, contentType, encrypted, null);
}
private static void sendBytes(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, byte[] bytes, String contentType, boolean encrypted, String deriveFingerprint) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType);
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
if (encrypted) response.headers().set("X-Nitro-Sec", "1");
response.headers().set("X-Nitro-Key-Fp", SERVER_KEY_FINGERPRINT);
if (deriveFingerprint != null && !deriveFingerprint.isEmpty()) response.headers().set("X-Nitro-Derive-Fp", deriveFingerprint);
applyCors(req, response);
boolean keepAlive = isKeepAlive(req);
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
var future = ctx.writeAndFlush(response);
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
}
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
applyCors(req, response);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
if (origin != null && !origin.isEmpty()) {
response.headers().set("Access-Control-Allow-Origin", origin);
response.headers().set("Vary", "Origin");
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Nitro-Key");
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
}
private static boolean isKeepAlive(FullHttpRequest req) {
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
return connection == null || !"close".equalsIgnoreCase(connection);
}
static String fingerprint(byte[] bytes) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(bytes);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 8 && i < hash.length; i++) {
builder.append(String.format("%02x", hash[i]));
}
return builder.toString();
} catch (Exception e) {
return "unknown";
}
}
static String getServerKeyFingerprint() {
return SERVER_KEY_FINGERPRINT;
}
static String toHex(byte[] bytes) {
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte value : bytes) {
builder.append(String.format("%02x", value & 0xff));
}
return builder.toString();
}
static byte[] fromHex(String hex) {
String normalized = hex == null ? "" : hex.trim();
if ((normalized.length() % 2) != 0) throw new IllegalArgumentException("Invalid encrypted hex payload.");
byte[] out = new byte[normalized.length() / 2];
for (int i = 0; i < out.length; i++) {
int high = Character.digit(normalized.charAt(i * 2), 16);
int low = Character.digit(normalized.charAt((i * 2) + 1), 16);
if (high < 0 || low < 0) throw new IllegalArgumentException("Invalid encrypted hex payload.");
out[i] = (byte) ((high << 4) | low);
}
return out;
}
private record CacheEntry(long modified, byte[] bytes) {}
}
@@ -44,7 +44,9 @@ public final class RememberJwtService {
}
private static int familyTtlDays() {
return Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30));
int configured = Emulator.getConfig().getInt("login.remember.duration.days", 0);
if (configured <= 0) configured = Emulator.getConfig().getInt("login.remember.days", 30);
return Math.max(1, configured);
}
private static long familyTtlSeconds() {
@@ -13,6 +13,7 @@ import com.eu.habbo.habbohotel.games.tag.TagGame;
import com.eu.habbo.habbohotel.items.ItemManager;
import com.eu.habbo.habbohotel.items.interactions.InteractionPostIt;
import com.eu.habbo.habbohotel.items.interactions.InteractionRoller;
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal;
import com.eu.habbo.habbohotel.items.interactions.games.football.InteractionFootballGate;
import com.eu.habbo.habbohotel.messenger.Messenger;
import com.eu.habbo.habbohotel.modtool.WordFilter;
@@ -116,6 +117,7 @@ public class PluginManager {
RoomManager.HOME_ROOM_ID = Emulator.getConfig().getInt("hotel.home.room");
WiredManager.MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count");
WiredManager.TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500);
WiredEffectSendSignal.MAX_SIGNAL_DEPTH = Emulator.getConfig().getInt("wired.signal.max.depth", 100);
WiredEngine.MAX_RECURSION_DEPTH = Emulator.getConfig().getInt("wired.abuse.max.recursion.depth", 10);
WiredEngine.MAX_EVENTS_PER_WINDOW = Emulator.getConfig().getInt("wired.abuse.max.events.per.window", 100);
WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000);
@@ -10,6 +10,11 @@ db.pool.maxsize=100
# Encrypt your traffic
crypto.ws.enabled=0
# Optional packet signing for encrypted WebSocket traffic.
crypto.ws.signing.enabled=false
# Optional persistent signing keys. Leave empty to auto-generate/persist them in emulator_settings.
crypto.ws.signing.public_key=
crypto.ws.signing.private_key=
#Game Configuration.
#Host IP. Most likely just 0.0.0.0 Use 127.0.0.1 if you want to play on LAN.
@@ -44,3 +49,23 @@ enc.enabled=false
enc.e=3
enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b93f06d29e8870f703a216257dec7c81de0058fea4cc5116f75e6efc4e9113513e45357dc3fd43d4efab5963ef178b78bd61e81a14c603b24c8bcce0a12230b320045498edc29282ff0603bc7b7dae8fc1b05b52b2f301a9dc783b7
enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b
# Nitro secure runtime assets. JSON files are read live from disk.
nitro.secure.assets.enabled=true
nitro.secure.api.enabled=true
# Secure runtime ECDH session TTL in seconds.
nitro.secure.session_ttl_sec=900
# Point this to your deployed Nitro `/configuration` folder when secure config assets are enabled.
nitro.secure.config.root=
nitro.secure.gamedata.root=
# Set a persistent secret when using Cloudflare / multiple backend requests.
nitro.secure.master_key=change-me-to-a-long-random-secret
# Remember-me login tokens.
login.remember.enabled=true
login.remember.duration.days=30
# Optional: set a persistent remember-me JWT secret here, otherwise one is generated and stored in emulator_settings.
login.remember.jwt.secret=
# Login news API.
login.news.limit=5