feat(mentions): server-side detection, persistence and packets

This commit is contained in:
simoleo89
2026-05-31 21:47:56 +02:00
parent 0aadd01493
commit e9129576a9
13 changed files with 440 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS `habbo_mentions` (
`id` INT NOT NULL AUTO_INCREMENT,
`target_user_id` INT NOT NULL,
`sender_user_id` INT NOT NULL,
`sender_username` VARCHAR(64) NOT NULL DEFAULT '',
`room_id` INT NOT NULL,
`room_name` VARCHAR(255) NOT NULL DEFAULT '',
`message` VARCHAR(255) NOT NULL DEFAULT '',
`mention_type` TINYINT NOT NULL DEFAULT 0,
`timestamp` INT NOT NULL DEFAULT 0,
`read` TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `idx_target_read` (`target_user_id`, `read`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
('mentions.enabled', '1'),
('mentions.room.aliases', 'amici,friends,all,everyone,tutti,room,stanza'),
('mentions.max.targets', '50'),
('mentions.cooldown.ms', '3000'),
('mentions.store.limit', '50');
@@ -8,6 +8,7 @@ import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
import com.eu.habbo.habbohotel.catalog.CatalogManager; import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.habbohotel.wheel.WheelManager; import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.habbohotel.soundboard.SoundboardManager; import com.eu.habbo.habbohotel.soundboard.SoundboardManager;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.habbohotel.commands.CommandHandler; import com.eu.habbo.habbohotel.commands.CommandHandler;
import com.eu.habbo.habbohotel.crafting.CraftingManager; import com.eu.habbo.habbohotel.crafting.CraftingManager;
import com.eu.habbo.habbohotel.guides.GuideManager; import com.eu.habbo.habbohotel.guides.GuideManager;
@@ -68,6 +69,7 @@ public class GameEnvironment {
private InfostandBackgroundManager infostandBackgroundManager; private InfostandBackgroundManager infostandBackgroundManager;
private WheelManager wheelManager; private WheelManager wheelManager;
private SoundboardManager soundboardManager; private SoundboardManager soundboardManager;
private MentionManager mentionManager;
public void load() throws Exception { public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading..."); LOGGER.info("GameEnvironment -> Loading...");
@@ -99,6 +101,7 @@ public class GameEnvironment {
this.infostandBackgroundManager = new InfostandBackgroundManager(); this.infostandBackgroundManager = new InfostandBackgroundManager();
this.wheelManager = new WheelManager(); this.wheelManager = new WheelManager();
this.soundboardManager = new SoundboardManager(); this.soundboardManager = new SoundboardManager();
this.mentionManager = new MentionManager();
this.roomManager.loadPublicRooms(); this.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator(); this.navigatorManager.loadNavigator();
@@ -202,6 +205,10 @@ public class GameEnvironment {
return this.petManager; return this.petManager;
} }
public MentionManager getMentionManager() {
return this.mentionManager;
}
public AchievementManager getAchievementManager() { public AchievementManager getAchievementManager() {
return this.achievementManager; return this.achievementManager;
} }
@@ -0,0 +1,90 @@
package com.eu.habbo.habbohotel.mentions;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import java.sql.ResultSet;
import java.sql.SQLException;
public class HabboMention {
public static final int TYPE_DIRECT = 0;
public static final int TYPE_ROOM = 1;
private final int id;
private final int targetUserId;
private final int senderUserId;
private final String senderUsername;
private final int roomId;
private final String roomName;
private final String message;
private final int mentionType;
private final int timestamp;
private final boolean read;
public HabboMention(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.targetUserId = set.getInt("target_user_id");
this.senderUserId = set.getInt("sender_user_id");
this.senderUsername = set.getString("sender_username");
this.roomId = set.getInt("room_id");
this.roomName = set.getString("room_name");
this.message = set.getString("message");
this.mentionType = set.getInt("mention_type");
this.timestamp = set.getInt("timestamp");
this.read = set.getInt("read") == 1;
}
public HabboMention(int targetUserId, int id, Habbo sender, Room room, String roomName, String message, int mentionType, int timestamp) {
this.id = id;
this.targetUserId = targetUserId;
this.senderUserId = sender.getHabboInfo().getId();
this.senderUsername = sender.getHabboInfo().getUsername();
this.roomId = room.getId();
this.roomName = roomName;
this.message = message;
this.mentionType = mentionType;
this.timestamp = timestamp;
this.read = false;
}
public int getId() {
return this.id;
}
public int getTargetUserId() {
return this.targetUserId;
}
public int getSenderUserId() {
return this.senderUserId;
}
public String getSenderUsername() {
return this.senderUsername;
}
public int getRoomId() {
return this.roomId;
}
public String getRoomName() {
return this.roomName;
}
public String getMessage() {
return this.message;
}
public int getMentionType() {
return this.mentionType;
}
public int getTimestamp() {
return this.timestamp;
}
public boolean isRead() {
return this.read;
}
}
@@ -0,0 +1,211 @@
package com.eu.habbo.habbohotel.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomChatType;
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.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class MentionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class);
private final ConcurrentHashMap<Integer, Long> cooldowns = new ConcurrentHashMap<>();
public boolean isEnabled() {
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
}
private Set<String> roomAliases() {
Set<String> aliases = new HashSet<>();
String raw = Emulator.getConfig().getValue("mentions.room.aliases", "amici,friends,all,everyone,tutti,room,stanza");
for (String alias : raw.split(",")) {
String trimmed = alias.trim().toLowerCase();
if (!trimmed.isEmpty()) {
aliases.add(trimmed);
}
}
return aliases;
}
public void process(Habbo sender, Room room, String message, RoomChatType type) {
try {
if (!this.isEnabled()) {
return;
}
if (sender == null || room == null || message == null) {
return;
}
if (message.isEmpty() || message.indexOf('@') < 0) {
return;
}
int senderId = sender.getHabboInfo().getId();
long now = System.currentTimeMillis();
long cooldownMs = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
Long last = this.cooldowns.get(senderId);
if (last != null && (now - last) < cooldownMs) {
return;
}
Set<String> aliases = this.roomAliases();
boolean roomBroadcast = false;
LinkedHashSet<String> directNicks = new LinkedHashSet<>();
for (String token : message.split("\\s+")) {
if (token.length() < 2 || token.charAt(0) != '@') {
continue;
}
String nick = token.substring(1).replaceAll("[^A-Za-z0-9_]", "").toLowerCase();
if (nick.isEmpty()) {
continue;
}
if (aliases.contains(nick)) {
roomBroadcast = true;
} else {
directNicks.add(nick);
}
}
if (!roomBroadcast && directNicks.isEmpty()) {
return;
}
int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50);
List<Habbo> targets = new ArrayList<>();
Set<Integer> seen = new HashSet<>();
if (roomBroadcast) {
for (Habbo habbo : room.getHabbos()) {
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
continue;
}
if (seen.add(habbo.getHabboInfo().getId())) {
targets.add(habbo);
}
if (targets.size() >= maxTargets) {
break;
}
}
} else {
for (String nick : directNicks) {
Habbo habbo = room.getHabbo(nick);
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
continue;
}
if (seen.add(habbo.getHabboInfo().getId())) {
targets.add(habbo);
}
if (targets.size() >= maxTargets) {
break;
}
}
}
if (targets.isEmpty()) {
return;
}
this.cooldowns.put(senderId, now);
int mentionType = roomBroadcast ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT;
int timestamp = Emulator.getIntUnixTimestamp();
String roomName = room.getName();
String storedMessage = message;
if (storedMessage.length() > 255) {
storedMessage = storedMessage.substring(0, 255);
}
for (Habbo target : targets) {
this.store(target, sender, room, storedMessage, mentionType, timestamp, roomName);
}
} catch (Exception e) {
LOGGER.error("Failed to process mentions.", e);
}
}
private void store(Habbo target, Habbo sender, Room room, String message, int mentionType, int timestamp, String roomName) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO habbo_mentions (target_user_id, sender_user_id, sender_username, room_id, room_name, message, mention_type, timestamp, `read`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, target.getHabboInfo().getId());
statement.setInt(2, sender.getHabboInfo().getId());
statement.setString(3, sender.getHabboInfo().getUsername());
statement.setInt(4, room.getId());
statement.setString(5, roomName);
statement.setString(6, message);
statement.setInt(7, mentionType);
statement.setInt(8, timestamp);
statement.executeUpdate();
int generatedId = 0;
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) {
generatedId = keys.getInt(1);
}
}
HabboMention mention = new HabboMention(target.getHabboInfo().getId(), generatedId, sender, room, roomName, message, mentionType, timestamp);
if (target.getClient() != null) {
target.getClient().sendResponse(new com.eu.habbo.messages.outgoing.mentions.MentionReceivedComposer(mention));
}
} catch (SQLException e) {
LOGGER.error("Failed to store mention.", e);
}
}
public List<HabboMention> getMentions(int userId, int limit) {
List<HabboMention> mentions = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM habbo_mentions WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, userId);
statement.setInt(2, limit);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
mentions.add(new HabboMention(set));
}
}
} catch (SQLException e) {
LOGGER.error("Failed to load mentions.", e);
}
return mentions;
}
public void markRead(int userId, int mode, int mentionId) {
String query = mode == 1
? "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND id = ?"
: "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ?";
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
statement.setInt(1, userId);
if (mode == 1) {
statement.setInt(2, mentionId);
}
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to mark mentions as read.", e);
}
}
}
@@ -39,6 +39,7 @@ import com.eu.habbo.messages.incoming.hotelview.*;
import com.eu.habbo.messages.incoming.inventory.*; import com.eu.habbo.messages.incoming.inventory.*;
import com.eu.habbo.messages.incoming.inventory.nickicons.*; import com.eu.habbo.messages.incoming.inventory.nickicons.*;
import com.eu.habbo.messages.incoming.inventory.prefixes.*; import com.eu.habbo.messages.incoming.inventory.prefixes.*;
import com.eu.habbo.messages.incoming.mentions.*;
import com.eu.habbo.messages.incoming.modtool.*; import com.eu.habbo.messages.incoming.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*; import com.eu.habbo.messages.incoming.navigator.*;
import com.eu.habbo.messages.incoming.polls.AnswerPollEvent; import com.eu.habbo.messages.incoming.polls.AnswerPollEvent;
@@ -426,6 +427,8 @@ public class PacketManager {
} }
void registerRooms() throws Exception { void registerRooms() throws Exception {
this.registerHandler(Incoming.RequestMentionsEvent, RequestMentionsEvent.class);
this.registerHandler(Incoming.MarkMentionsReadEvent, MarkMentionsReadEvent.class);
this.registerHandler(Incoming.RequestRoomLoadEvent, RequestRoomLoadEvent.class); this.registerHandler(Incoming.RequestRoomLoadEvent, RequestRoomLoadEvent.class);
this.registerHandler(Incoming.RequestHeightmapEvent, RequestRoomHeightmapEvent.class); this.registerHandler(Incoming.RequestHeightmapEvent, RequestRoomHeightmapEvent.class);
this.registerHandler(Incoming.RequestRoomHeightmapEvent, RequestRoomHeightmapEvent.class); this.registerHandler(Incoming.RequestRoomHeightmapEvent, RequestRoomHeightmapEvent.class);
@@ -496,4 +496,6 @@ public class Incoming {
public static final int WheelAdminSavePrizesEvent = 9305; public static final int WheelAdminSavePrizesEvent = 9305;
public static final int SoundboardPlayEvent = 9306; public static final int SoundboardPlayEvent = 9306;
public static final int SoundboardSetEnabledEvent = 9307; public static final int SoundboardSetEnabledEvent = 9307;
public static final int RequestMentionsEvent = 4803;
public static final int MarkMentionsReadEvent = 4804;
} }
@@ -0,0 +1,15 @@
package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.incoming.MessageHandler;
public class MarkMentionsReadEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int userId = this.client.getHabbo().getHabboInfo().getId();
int mode = this.packet.readInt();
int mentionId = this.packet.readInt();
Emulator.getGameEnvironment().getMentionManager().markRead(userId, mode, mentionId);
}
}
@@ -0,0 +1,19 @@
package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.mentions.HabboMention;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.mentions.MentionsListComposer;
import java.util.List;
public class RequestMentionsEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int userId = this.client.getHabbo().getHabboInfo().getId();
int limit = Emulator.getConfig().getInt("mentions.store.limit", 50);
List<HabboMention> mentions = Emulator.getGameEnvironment().getMentionManager().getMentions(userId, limit);
this.client.sendResponse(new MentionsListComposer(mentions));
}
}
@@ -34,6 +34,9 @@ public class RoomUserShoutEvent extends MessageHandler {
if (RoomChatMessage.SAVE_ROOM_CHATS) { if (RoomChatMessage.SAVE_ROOM_CHATS) {
Emulator.getThreading().run(message); Emulator.getThreading().run(message);
} }
Emulator.getGameEnvironment().getMentionManager()
.process(this.client.getHabbo(), this.client.getHabbo().getHabboInfo().getCurrentRoom(), message.getMessage(), RoomChatType.SHOUT);
} }
} else { } else {
String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + ""); String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + "");
@@ -36,6 +36,9 @@ public class RoomUserTalkEvent extends MessageHandler {
if (RoomChatMessage.SAVE_ROOM_CHATS) { if (RoomChatMessage.SAVE_ROOM_CHATS) {
Emulator.getThreading().run(message); Emulator.getThreading().run(message);
} }
Emulator.getGameEnvironment().getMentionManager()
.process(this.client.getHabbo(), room, message.getMessage(), RoomChatType.TALK);
} }
} else { } else {
String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + ""); String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + "");
@@ -602,5 +602,7 @@ public class Outgoing {
public static final int WheelAdminPrizesComposer = 9404; public static final int WheelAdminPrizesComposer = 9404;
public static final int SoundboardSettingsComposer = 9405; public static final int SoundboardSettingsComposer = 9405;
public static final int SoundboardPlayComposer = 9406; public static final int SoundboardPlayComposer = 9406;
public static final int MentionReceivedComposer = 4801;
public static final int MentionsListComposer = 4802;
} }
@@ -0,0 +1,28 @@
package com.eu.habbo.messages.outgoing.mentions;
import com.eu.habbo.habbohotel.mentions.HabboMention;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class MentionReceivedComposer extends MessageComposer {
private final HabboMention mention;
public MentionReceivedComposer(HabboMention mention) {
this.mention = mention;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.MentionReceivedComposer);
this.response.appendInt(this.mention.getId());
this.response.appendInt(this.mention.getSenderUserId());
this.response.appendString(this.mention.getSenderUsername());
this.response.appendInt(this.mention.getRoomId());
this.response.appendString(this.mention.getRoomName());
this.response.appendString(this.mention.getMessage());
this.response.appendInt(this.mention.getMentionType());
this.response.appendInt(this.mention.getTimestamp());
return this.response;
}
}
@@ -0,0 +1,36 @@
package com.eu.habbo.messages.outgoing.mentions;
import com.eu.habbo.habbohotel.mentions.HabboMention;
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 MentionsListComposer extends MessageComposer {
private final List<HabboMention> mentions;
public MentionsListComposer(List<HabboMention> mentions) {
this.mentions = mentions;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.MentionsListComposer);
this.response.appendInt(this.mentions.size());
for (HabboMention mention : this.mentions) {
this.response.appendInt(mention.getId());
this.response.appendInt(mention.getSenderUserId());
this.response.appendString(mention.getSenderUsername());
this.response.appendInt(mention.getRoomId());
this.response.appendString(mention.getRoomName());
this.response.appendString(mention.getMessage());
this.response.appendInt(mention.getMentionType());
this.response.appendInt(mention.getTimestamp());
this.response.appendBoolean(mention.isRead());
}
return this.response;
}
}