feat: custom prefix system with effects and admin commands

Backend implementation:
- UserPrefix model with effect field, DB CRUD operations
- PrefixesComponent inventory management
- PurchasePrefixEvent with settings, blacklist and currency validation
- Composers: UserPrefixes, PrefixReceived, ActivePrefixUpdated
- RoomChatMessage serializes active prefix (text, color, icon, effect)
- Admin commands: giveprefix, listprefixes, removeprefix, prefixblacklist
- CustomPrefixLayout catalog page
- SQL setup: user_prefixes, custom_prefix_settings, custom_prefix_blacklist tables
This commit is contained in:
simoleo89
2026-03-20 17:14:15 +01:00
parent 77fea6b9fd
commit 6b4e6a8759
23 changed files with 1038 additions and 1 deletions
@@ -0,0 +1,115 @@
-- ============================================================
-- Custom Prefix System - Complete Setup
-- ============================================================
-- 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,
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`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
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
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Example blacklist entries (customize as needed)
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'),
('staff'),
('mod'),
('owner');
-- 4. Add effect column (if table already exists without it)
-- ALTER TABLE `user_prefixes` ADD COLUMN IF NOT EXISTS `effect` VARCHAR(50) NOT NULL DEFAULT '' AFTER `icon`;
-- ============================================================
-- Catalog page for custom prefixes
-- ============================================================
-- NOTE: Adjust parent_id to match your catalog parent category ID.
-- Example: parent_id = -1 for root, or the ID of your "Extra" / "Specials" category
INSERT INTO `catalog_pages` (
`parent_id`, `caption`, `caption_save`, `icon_image`, `visible`, `enabled`,
`min_rank`, `page_layout`, `page_strings_1`, `page_strings_2`
) VALUES (
-1,
'Custom Prefix',
'custom_prefix',
1,
1,
1,
1,
'custom_prefix',
'Create your own custom prefix!\rChoose text, colors, icon and effects to stand out in chat.',
''
);
-- ============================================================
-- Command texts (insert into emulator_texts if not present)
-- ============================================================
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command
('commands.keys.cmd_give_prefix', 'giveprefix'),
('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix <username> <text> <color> [icon] [effect]'),
('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'),
('commands.error.cmd_give_prefix.too_long', 'Prefix text is too long (max 15 characters).'),
('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'),
-- ListPrefixes command
('commands.keys.cmd_list_prefixes', 'listprefixes'),
('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes <username>'),
('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'),
('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'),
-- RemovePrefix command
('commands.keys.cmd_remove_prefix', 'removeprefix'),
('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix <username> <id|all>'),
('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'),
('commands.error.cmd_remove_prefix.invalid_id', 'Invalid prefix ID. Must be a number or "all".'),
('commands.error.cmd_remove_prefix.not_found', 'Prefix not found for this user.'),
('commands.succes.cmd_remove_prefix', 'Prefix #%id% removed from %user%.'),
('commands.succes.cmd_remove_prefix.all', 'All prefixes removed from %user%.'),
-- PrefixBlacklist command
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
('commands.error.cmd_prefix_blacklist.usage', 'Usage: :prefixblacklist <add|remove|list> [word]'),
('commands.error.cmd_prefix_blacklist.empty_word', 'Word cannot be empty.'),
('commands.succes.cmd_prefix_blacklist.header', 'Blacklisted prefix words:'),
('commands.succes.cmd_prefix_blacklist.empty', 'No blacklisted words.'),
('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'),
('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.');
-- ============================================================
-- Permissions for prefix commands (add to permissions table)
-- ============================================================
INSERT IGNORE INTO `permissions` (`id`, `rank_id`, `permission_name`, `setting_type`) VALUES
(NULL, 7, 'cmd_give_prefix', '1'),
(NULL, 7, 'cmd_list_prefixes', '1'),
(NULL, 7, 'cmd_remove_prefix', '1'),
(NULL, 7, 'cmd_prefix_blacklist', '1');
@@ -173,6 +173,9 @@ public class CatalogManager {
case mad_money:
this.put(layout.name().toLowerCase(), MadMoneyLayout.class);
break;
case custom_prefix:
this.put(layout.name().toLowerCase(), CustomPrefixLayout.class);
break;
case default_3x3:
default:
this.put("default_3x3", Default_3x3Layout.class);
@@ -43,5 +43,6 @@ public enum CatalogPageLayouts {
builders_club_loyalty,
monkey,
niko,
mad_money
mad_money,
custom_prefix
}
@@ -0,0 +1,27 @@
package com.eu.habbo.habbohotel.catalog.layouts;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.messages.ServerMessage;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CustomPrefixLayout extends CatalogPage {
public CustomPrefixLayout(ResultSet set) throws SQLException {
super(set);
}
@Override
public void serialize(ServerMessage message) {
message.appendString("custom_prefix");
message.appendInt(3);
message.appendString(super.getHeaderImage());
message.appendString(super.getTeaserImage());
message.appendString(super.getSpecialImage());
message.appendInt(3);
message.appendString(super.getTextOne());
message.appendString(super.getTextDetails());
message.appendString(super.getTextTeaser());
}
}
@@ -297,6 +297,10 @@ public class CommandHandler {
addCommand(new SoftKickCommand());
addCommand(new SubscriptionCommand());
addCommand(new UpdateChatBubblesCommand());
addCommand(new GivePrefixCommand());
addCommand(new ListPrefixesCommand());
addCommand(new RemovePrefixCommand());
addCommand(new PrefixBlacklistCommand());
addCommand(new TestCommand());
}
@@ -0,0 +1,66 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer;
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
public class GivePrefixCommand extends Command {
public GivePrefixCommand() {
super("cmd_give_prefix", Emulator.getTexts().getValue("commands.keys.cmd_give_prefix").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 4) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String targetName = params[1];
String text = params[2];
String color = params[3];
String icon = params.length > 4 ? params[4] : "";
String effect = params.length > 5 ? params[5] : "";
// Validate color
String[] colorParts = color.split(",");
for (String part : colorParts) {
if (!part.matches("^#[0-9A-Fa-f]{6}$")) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.invalid_color"), RoomChatMessageBubbles.ALERT);
return true;
}
}
if (text.length() > 15) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.too_long"), RoomChatMessageBubbles.ALERT);
return true;
}
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetName);
if (target == null) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.user_not_found"), RoomChatMessageBubbles.ALERT);
return true;
}
UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect);
prefix.run();
target.getInventory().getPrefixesComponent().addPrefix(prefix);
target.getClient().sendResponse(new PrefixReceivedComposer(prefix));
target.getClient().sendResponse(new UserPrefixesComposer(target));
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_give_prefix")
.replace("%user%", targetName)
.replace("%prefix%", text),
RoomChatMessageBubbles.ALERT
);
return true;
}
}
@@ -0,0 +1,59 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserPrefix;
import java.util.List;
public class ListPrefixesCommand extends Command {
public ListPrefixesCommand() {
super("cmd_list_prefixes", Emulator.getTexts().getValue("commands.keys.cmd_list_prefixes").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 2) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_list_prefixes.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String targetName = params[1];
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetName);
if (target == null) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_list_prefixes.user_not_found"), RoomChatMessageBubbles.ALERT);
return true;
}
List<UserPrefix> prefixes = target.getInventory().getPrefixesComponent().getPrefixes();
if (prefixes.isEmpty()) {
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_list_prefixes.empty").replace("%user%", targetName),
RoomChatMessageBubbles.ALERT
);
return true;
}
StringBuilder sb = new StringBuilder();
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_list_prefixes.header").replace("%user%", targetName)).append("\r");
for (UserPrefix prefix : prefixes) {
sb.append("ID: ").append(prefix.getId())
.append(" | {").append(prefix.getText()).append("}")
.append(" | Color: ").append(prefix.getColor())
.append(prefix.getIcon().isEmpty() ? "" : " | Icon: " + prefix.getIcon())
.append(prefix.getEffect().isEmpty() ? "" : " | Effect: " + prefix.getEffect())
.append(prefix.isActive() ? " [ACTIVE]" : "")
.append("\r");
}
gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT);
return true;
}
}
@@ -0,0 +1,98 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class PrefixBlacklistCommand extends Command {
private static final Logger LOGGER = LoggerFactory.getLogger(PrefixBlacklistCommand.class);
public PrefixBlacklistCommand() {
super("cmd_prefix_blacklist", Emulator.getTexts().getValue("commands.keys.cmd_prefix_blacklist").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 2) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String action = params[1].toLowerCase();
if (action.equals("list")) {
StringBuilder sb = new StringBuilder();
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.header")).append("\r");
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist ORDER BY word")) {
try (ResultSet set = statement.executeQuery()) {
int count = 0;
while (set.next()) {
sb.append("- ").append(set.getString("word")).append("\r");
count++;
}
if (count == 0) {
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.empty"));
}
}
} catch (SQLException e) {
LOGGER.error("Error listing prefix blacklist", e);
}
gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT);
return true;
}
if (params.length < 3) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String word = params[2].toLowerCase().trim();
if (word.isEmpty()) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.empty_word"), RoomChatMessageBubbles.ALERT);
return true;
}
if (action.equals("add")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO custom_prefix_blacklist (word) VALUES (?)")) {
statement.setString(1, word);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Error adding prefix blacklist word", e);
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.added").replace("%word%", word),
RoomChatMessageBubbles.ALERT
);
} else if (action.equals("remove")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM custom_prefix_blacklist WHERE word = ?")) {
statement.setString(1, word);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Error removing prefix blacklist word", e);
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.removed").replace("%word%", word),
RoomChatMessageBubbles.ALERT
);
} else {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
}
return true;
}
}
@@ -0,0 +1,81 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
import java.util.List;
public class RemovePrefixCommand extends Command {
public RemovePrefixCommand() {
super("cmd_remove_prefix", Emulator.getTexts().getValue("commands.keys.cmd_remove_prefix").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 3) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String targetName = params[1];
String prefixIdStr = params[2];
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetName);
if (target == null) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.user_not_found"), RoomChatMessageBubbles.ALERT);
return true;
}
if (prefixIdStr.equalsIgnoreCase("all")) {
List<UserPrefix> prefixes = target.getInventory().getPrefixesComponent().getPrefixes();
for (UserPrefix prefix : prefixes) {
prefix.needsDelete(true);
Emulator.getThreading().run(prefix);
}
// Clear in-memory
for (UserPrefix prefix : prefixes) {
target.getInventory().getPrefixesComponent().removePrefix(prefix);
}
target.getClient().sendResponse(new UserPrefixesComposer(target));
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_remove_prefix.all").replace("%user%", targetName),
RoomChatMessageBubbles.ALERT
);
} else {
int prefixId;
try {
prefixId = Integer.parseInt(prefixIdStr);
} catch (NumberFormatException e) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.invalid_id"), RoomChatMessageBubbles.ALERT);
return true;
}
UserPrefix prefix = target.getInventory().getPrefixesComponent().getPrefix(prefixId);
if (prefix == null) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.not_found"), RoomChatMessageBubbles.ALERT);
return true;
}
target.getInventory().getPrefixesComponent().removePrefix(prefix);
prefix.needsDelete(true);
Emulator.getThreading().run(prefix);
target.getClient().sendResponse(new UserPrefixesComposer(target));
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_remove_prefix").replace("%user%", targetName).replace("%id%", String.valueOf(prefixId)),
RoomChatMessageBubbles.ALERT
);
}
return true;
}
}
@@ -202,6 +202,25 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable {
message.appendInt(0);
message.appendString(this.RoomChatColour); //Added packet for room chat
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);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
@@ -22,6 +22,7 @@ public class HabboInventory {
private EffectsComponent effectsComponent;
private ItemsComponent itemsComponent;
private PetsComponent petsComponent;
private PrefixesComponent prefixesComponent;
public HabboInventory(Habbo habbo) {
this.habbo = habbo;
@@ -61,6 +62,12 @@ public class HabboInventory {
LOGGER.error("Caught exception", e);
}
try {
this.prefixesComponent = new PrefixesComponent(this.habbo);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
this.items = MarketPlace.getOwnOffers(this.habbo);
}
@@ -112,6 +119,14 @@ public class HabboInventory {
this.petsComponent = petsComponent;
}
public PrefixesComponent getPrefixesComponent() {
return this.prefixesComponent;
}
public void setPrefixesComponent(PrefixesComponent prefixesComponent) {
this.prefixesComponent = prefixesComponent;
}
public void dispose() {
this.badgesComponent.dispose();
this.botsComponent.dispose();
@@ -119,6 +134,7 @@ public class HabboInventory {
this.itemsComponent.dispose();
this.petsComponent.dispose();
this.wardrobeComponent.dispose();
this.prefixesComponent.dispose();
this.badgesComponent = null;
this.botsComponent = null;
@@ -126,6 +142,7 @@ public class HabboInventory {
this.itemsComponent = null;
this.petsComponent = null;
this.wardrobeComponent = null;
this.prefixesComponent = null;
}
public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) {
@@ -0,0 +1,122 @@
package com.eu.habbo.habbohotel.users;
import com.eu.habbo.Emulator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
public class UserPrefix implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(UserPrefix.class);
private int id;
private final int userId;
private String text;
private String color;
private String icon;
private String effect;
private boolean active;
private boolean needsInsert;
private boolean needsUpdate;
private boolean needsDelete;
public UserPrefix(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.userId = set.getInt("user_id");
this.text = set.getString("text");
this.color = set.getString("color");
this.icon = set.getString("icon");
if (this.icon == null) this.icon = "";
this.effect = set.getString("effect");
if (this.effect == null) this.effect = "";
this.active = set.getBoolean("active");
this.needsInsert = false;
this.needsUpdate = false;
this.needsDelete = false;
}
public UserPrefix(int userId, String text, String color, String icon, String effect) {
this.id = 0;
this.userId = userId;
this.text = text;
this.color = color;
this.icon = icon != null ? icon : "";
this.effect = effect != null ? effect : "";
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_prefixes (user_id, text, color, icon, effect, active) 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.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_prefixes 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_prefixes SET text = ?, color = ?, icon = ?, effect = ?, active = ? 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.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 getText() { return this.text; }
public void setText(String text) { this.text = text; }
public String getColor() { return this.color; }
public void setColor(String color) { this.color = color; }
public String getIcon() { return this.icon; }
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 boolean isActive() { return this.active; }
public void setActive(boolean active) {
this.active = active;
this.needsUpdate = true;
}
public void needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; }
public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; }
public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; }
}
@@ -0,0 +1,105 @@
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.UserPrefix;
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 PrefixesComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(PrefixesComponent.class);
private final List<UserPrefix> prefixes = new ArrayList<>();
private final Habbo habbo;
public PrefixesComponent(Habbo habbo) {
this.habbo = habbo;
this.loadPrefixes();
}
private void loadPrefixes() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM user_prefixes WHERE user_id = ?")) {
statement.setInt(1, this.habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
this.prefixes.add(new UserPrefix(set));
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public List<UserPrefix> getPrefixes() {
synchronized (this.prefixes) {
return new ArrayList<>(this.prefixes);
}
}
public UserPrefix getActivePrefix() {
synchronized (this.prefixes) {
for (UserPrefix prefix : this.prefixes) {
if (prefix.isActive()) return prefix;
}
}
return null;
}
public UserPrefix getPrefix(int id) {
synchronized (this.prefixes) {
for (UserPrefix prefix : this.prefixes) {
if (prefix.getId() == id) return prefix;
}
}
return null;
}
public void addPrefix(UserPrefix prefix) {
synchronized (this.prefixes) {
this.prefixes.add(prefix);
}
}
public void removePrefix(UserPrefix prefix) {
synchronized (this.prefixes) {
this.prefixes.remove(prefix);
}
}
public void setActive(int prefixId) {
synchronized (this.prefixes) {
for (UserPrefix prefix : this.prefixes) {
boolean shouldBeActive = prefix.getId() == prefixId;
if (prefix.isActive() != shouldBeActive) {
prefix.setActive(shouldBeActive);
Emulator.getThreading().run(prefix);
}
}
}
}
public void deactivateAll() {
synchronized (this.prefixes) {
for (UserPrefix prefix : this.prefixes) {
if (prefix.isActive()) {
prefix.setActive(false);
Emulator.getThreading().run(prefix);
}
}
}
}
public void dispose() {
synchronized (this.prefixes) {
this.prefixes.clear();
}
}
}
@@ -34,6 +34,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.prefixes.*;
import com.eu.habbo.messages.incoming.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*;
import com.eu.habbo.messages.incoming.polls.AnswerPollEvent;
@@ -370,6 +371,12 @@ public class PacketManager {
this.registerHandler(Incoming.RequestInventoryPetsEvent, RequestInventoryPetsEvent.class);
this.registerHandler(Incoming.RequestInventoryPetDelete, RequestInventoryPetDelete.class);
this.registerHandler(Incoming.RequestInventoryBadgeDelete, RequestInventoryBadgeDelete.class);
// Custom Prefixes
this.registerHandler(Incoming.RequestUserPrefixesEvent, RequestUserPrefixesEvent.class);
this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class);
this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class);
this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.class);
}
void registerRooms() throws Exception {
@@ -409,4 +409,10 @@ public class Incoming {
public static final int UpdateFurniturePositionEvent = 10019;
public static final int RequestInventoryPetDelete = 10030;
public static final int RequestInventoryBadgeDelete = 10031;
// Custom Prefixes
public static final int RequestUserPrefixesEvent = 7011;
public static final int SetActivePrefixEvent = 7012;
public static final int DeletePrefixEvent = 7013;
public static final int PurchasePrefixEvent = 7014;
}
@@ -0,0 +1,23 @@
package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
public class DeletePrefixEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int prefixId = this.packet.readInt();
UserPrefix prefix = this.client.getHabbo().getInventory().getPrefixesComponent().getPrefix(prefixId);
if (prefix == null) return;
this.client.getHabbo().getInventory().getPrefixesComponent().removePrefix(prefix);
prefix.needsDelete(true);
Emulator.getThreading().run(prefix);
this.client.sendResponse(new UserPrefixesComposer(this.client.getHabbo()));
}
}
@@ -0,0 +1,145 @@
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.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
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;
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 PurchasePrefixEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class);
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
String text = this.packet.readString();
String color = this.packet.readString();
String icon = this.packet.readString();
String effect = this.packet.readString();
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
// Load settings
int maxLength = getSettingInt("max_length", 15);
int minRank = getSettingInt("min_rank_to_buy", 1);
int priceCredits = getSettingInt("price_credits", 5);
int pricePoints = getSettingInt("price_points", 0);
int pointsType = getSettingInt("points_type", 0);
// Validate text
text = text.trim();
if (text.isEmpty() || text.length() > maxLength) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Prefix text is invalid or too long (max " + maxLength + " characters)."));
return;
}
// Validate color (single hex or comma-separated multi hex for per-letter colors)
String[] colorParts = color.split(",");
for (String part : colorParts) {
if (!part.matches("^#[0-9A-Fa-f]{6}$")) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid color format."));
return;
}
}
// Check rank
if (habbo.getHabboInfo().getRank().getId() < minRank) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Your rank is too low to purchase prefixes."));
return;
}
// Check blacklist
if (isBlacklisted(text)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "This prefix contains a blocked word."));
return;
}
// Check credits
if (priceCredits > 0 && habbo.getHabboInfo().getCredits() < priceCredits) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits."));
return;
}
// Check points
if (pricePoints > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < pricePoints) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
return;
}
// Deduct currency
if (priceCredits > 0) {
habbo.getHabboInfo().addCredits(-priceCredits);
this.client.sendResponse(new UserCreditsComposer(habbo));
}
if (pricePoints > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -pricePoints);
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();
// Create prefix
UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect);
prefix.run(); // Insert into DB synchronously to get the ID
habbo.getInventory().getPrefixesComponent().addPrefix(prefix);
this.client.sendResponse(new PrefixReceivedComposer(prefix));
}
private int getSettingInt(String key, int defaultValue) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT `value` FROM custom_prefix_settings WHERE key_name = ?")) {
statement.setString(1, key);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return Integer.parseInt(set.getString("value"));
}
}
} catch (SQLException | NumberFormatException e) {
LOGGER.error("Error reading prefix setting: " + key, e);
}
return defaultValue;
}
private boolean isBlacklisted(String text) {
String lowerText = text.toLowerCase();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist")) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
if (lowerText.contains(set.getString("word").toLowerCase())) {
return true;
}
}
}
} catch (SQLException e) {
LOGGER.error("Error checking prefix blacklist", e);
}
return false;
}
}
@@ -0,0 +1,11 @@
package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
public class RequestUserPrefixesEvent extends MessageHandler {
@Override
public void handle() throws Exception {
this.client.sendResponse(new UserPrefixesComposer(this.client.getHabbo()));
}
}
@@ -0,0 +1,25 @@
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;
public class SetActivePrefixEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int prefixId = this.packet.readInt();
if (prefixId == 0) {
this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll();
this.client.sendResponse(new ActivePrefixUpdatedComposer(null));
return;
}
UserPrefix prefix = this.client.getHabbo().getInventory().getPrefixesComponent().getPrefix(prefixId);
if (prefix == null) return;
this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId);
this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix));
}
}
@@ -554,4 +554,9 @@ public class Outgoing {
public static final int SnowStormUserRematchedComposer = 5029;
// Custom Prefixes
public static final int UserPrefixesComposer = 7001;
public static final int PrefixReceivedComposer = 7002;
public static final int ActivePrefixUpdatedComposer = 7003;
}
@@ -0,0 +1,35 @@
package com.eu.habbo.messages.outgoing.inventory.prefixes;
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;
public class ActivePrefixUpdatedComposer extends MessageComposer {
private final UserPrefix prefix;
public ActivePrefixUpdatedComposer(UserPrefix prefix) {
this.prefix = prefix;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.ActivePrefixUpdatedComposer);
if (this.prefix != null) {
this.response.appendInt(this.prefix.getId());
this.response.appendString(this.prefix.getText());
this.response.appendString(this.prefix.getColor());
this.response.appendString(this.prefix.getIcon());
this.response.appendString(this.prefix.getEffect());
} else {
this.response.appendInt(0);
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
}
return this.response;
}
}
@@ -0,0 +1,25 @@
package com.eu.habbo.messages.outgoing.inventory.prefixes;
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;
public class PrefixReceivedComposer extends MessageComposer {
private final UserPrefix prefix;
public PrefixReceivedComposer(UserPrefix prefix) {
this.prefix = prefix;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.PrefixReceivedComposer);
this.response.appendInt(this.prefix.getId());
this.response.appendString(this.prefix.getText());
this.response.appendString(this.prefix.getColor());
this.response.appendString(this.prefix.getIcon());
this.response.appendString(this.prefix.getEffect());
return this.response;
}
}
@@ -0,0 +1,38 @@
package com.eu.habbo.messages.outgoing.inventory.prefixes;
import com.eu.habbo.habbohotel.users.Habbo;
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 java.util.List;
public class UserPrefixesComposer extends MessageComposer {
private final Habbo habbo;
public UserPrefixesComposer(Habbo habbo) {
this.habbo = habbo;
}
@Override
protected ServerMessage composeInternal() {
if (this.habbo == null) return null;
List<UserPrefix> prefixes = this.habbo.getInventory().getPrefixesComponent().getPrefixes();
this.response.init(Outgoing.UserPrefixesComposer);
this.response.appendInt(prefixes.size());
for (UserPrefix prefix : prefixes) {
this.response.appendInt(prefix.getId());
this.response.appendString(prefix.getText());
this.response.appendString(prefix.getColor());
this.response.appendString(prefix.getIcon());
this.response.appendString(prefix.getEffect());
this.response.appendInt(prefix.isActive() ? 1 : 0);
}
return this.response;
}
}