Compare commits

...

18 Commits

Author SHA1 Message Date
github-actions[bot] 2c0ef9873c 🆙 Bump version to 4.2.36 [skip ci] 2026-06-04 08:44:19 +00:00
DuckieTM dadc1b8aaf Merge pull request #153 from duckietm/dev
Dev
2026-06-04 10:43:21 +02:00
duckietm 85758b53fa 🆙 Updates Mention 2026-06-04 10:43:05 +02:00
DuckieTM 2171b5f2ec Merge pull request #152 from medievalshell/feat/mentions-hotelwide-figure
feat(mentions): hotel-wide @nick delivery + sender figure + disable-m…
2026-06-04 08:50:49 +02:00
medievalshell 46306c8205 feat(mentions): hotel-wide @nick delivery + sender figure + disable-mention persistence
- resolveHabbo() falls back to a hotel-wide online lookup so a direct @nick
  mention reaches the target even when they are in a different room (was
  resolved only within the sender's room).
- HabboMention now carries the sender figure (live from the sender Habbo,
  history from a users.look JOIN); MentionReceived/MentionsList composers
  append it so the client can render the sender avatar in the notification.
- 009: add users_settings.mentions_enabled / mass_mentions_enabled columns
  so :disablementions / :disablemassmentions actually persist.
2026-06-04 01:27:45 +02:00
github-actions[bot] fadec887cd 🆙 Bump version to 4.2.35 [skip ci] 2026-06-03 14:45:16 +00:00
DuckieTM e614c1d64f Merge pull request #150 from duckietm/dev
Merge pull request #149 from duckietm/main
2026-06-03 16:44:04 +02:00
DuckieTM e7deea7d9d Merge pull request #149 from duckietm/main
sync to dev
2026-06-03 16:39:01 +02:00
github-actions[bot] 44ea3abd4e 🆙 Bump version to 4.2.34 [skip ci] 2026-06-03 14:37:38 +00:00
DuckieTM 609cd20ab2 Merge pull request #143 from simoleo89/feat/command-autocomplete-refactor
Structure commands alert output
2026-06-03 16:36:33 +02:00
github-actions[bot] 717a7f184f 🆙 Bump version to 4.2.33 [skip ci] 2026-06-03 14:23:40 +00:00
DuckieTM 2862446686 Merge pull request #148 from duckietm/dev
🆙 More updates mentions
2026-06-03 16:22:39 +02:00
duckietm e97e680006 🆙 More updates mentions 2026-06-03 16:20:02 +02:00
github-actions[bot] 7e59dca192 🆙 Bump version to 4.2.32 [skip ci] 2026-06-03 12:20:44 +00:00
DuckieTM 1109d53720 Merge pull request #147 from duckietm/dev
Dev
2026-06-03 14:19:42 +02:00
duckietm f12363a5b7 Merge branch 'dev' of https://github.com/duckietm/Arcturus-Morningstar-Extended into dev 2026-06-03 14:17:28 +02:00
duckietm 7d4ffec74e 🆙 Small Fixes mention 2026-06-03 14:17:25 +02:00
simoleo89 9f36d95dbc fix(commands): structure commands alert output 2026-06-02 18:34:50 +02:00
15 changed files with 531 additions and 91 deletions
+82 -19
View File
@@ -1,26 +1,89 @@
CREATE TABLE IF NOT EXISTS `habbo_mentions` ( CREATE TABLE IF NOT EXISTS `habbo_mentions` (
`id` INT NOT NULL AUTO_INCREMENT, `id` INT(11) NOT NULL AUTO_INCREMENT,
`target_user_id` INT NOT NULL, `target_user_id` INT(11) NOT NULL,
`sender_user_id` INT NOT NULL, `sender_user_id` INT(11) NOT NULL,
`sender_username` VARCHAR(64) NOT NULL DEFAULT '', `sender_username` VARCHAR(64) NOT NULL DEFAULT '',
`room_id` INT NOT NULL, `room_id` INT(11) NOT NULL DEFAULT 0,
`room_name` VARCHAR(255) NOT NULL DEFAULT '', `room_name` VARCHAR(64) NOT NULL DEFAULT '',
`message` VARCHAR(255) NOT NULL DEFAULT '', `message` VARCHAR(255) NOT NULL DEFAULT '',
`mention_type` TINYINT NOT NULL DEFAULT 0, `mention_type` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0 = direct (@nick), 1 = broadcast (@all/@friends/@room)',
`timestamp` INT NOT NULL DEFAULT 0, `timestamp` INT(11) NOT NULL DEFAULT 0,
`read` TINYINT NOT NULL DEFAULT 0, `read` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `idx_target_read` (`target_user_id`, `read`) KEY `idx_target_id` (`target_user_id`, `id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; KEY `idx_target_unread` (`target_user_id`, `read`),
KEY `idx_target_timestamp` (`target_user_id`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
('mentions.enabled', '1'),
('mentions.room.aliases', 'amici,friends,all,everyone,tutti,room,stanza'),
('mentions.max.targets', '50'), INSERT INTO `permission_definitions`
('mentions.cooldown.ms', '3000'), (`permission_key`, `max_value`, `comment`,
('mentions.store.limit', '50'); `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`, `rank_8`)
VALUES
('acc_mention_everyone', 1,
'Allow sending @all / @everyone / @tutti broadcast mentions (hotel-wide).',
0, 0, 0, 0, 1, 1, 1, 1),
('acc_mention_friends', 1,
'Allow sending @friends / @amici broadcast mentions (sender''s online buddies).',
0, 0, 0, 0, 1, 1, 1, 1),
('cmd_disablementions', 1,
'Allow toggling :disablementions to stop receiving any @mention notifications.',
1, 1, 1, 1, 1, 1, 1, 1),
('cmd_disablemassmentions', 1,
'Allow toggling :disablemassmentions to stop receiving broadcast mentions (direct @nick still works).',
1, 1, 1, 1, 1, 1, 1, 1)
ON DUPLICATE KEY UPDATE
`comment` = VALUES(`comment`);
-- ----------------------------------------------------------------------------
-- 3. Emulator settings: cooldowns, caps and alias lists
--
-- Only inserted when missing - existing tuned values are preserved.
-- ----------------------------------------------------------------------------
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('mentions.enabled', '1',
'Master switch. 1 = process @mentions, 0 = disable the feature entirely.'),
('mentions.max.targets', '50',
'Hard cap on how many users a single broadcast (@all / @friends / @room) can fan out to.'),
('mentions.cooldown.ms', '3000',
'Per-sender cooldown between any two mentions, in milliseconds.'),
('mentions.room.cooldown.ms', '15000',
'Extra per-sender cooldown for broadcast mentions (@all / @friends / @room) on top of mentions.cooldown.ms.'),
('mentions.store.limit', '50',
'Number of mentions returned in the initial RequestMentionsList response.'),
('mentions.request.cooldown.ms', '2000',
'Per-user cooldown between RequestMentionsList packets.'),
('mentions.markread.cooldown.ms', '500',
'Per-user cooldown between mark-single-as-read packets.'),
('mentions.markall.cooldown.ms', '5000',
'Per-user cooldown between mark-all-as-read packets (bulk DB update).'),
('mentions.delete.cooldown.ms', '500',
'Per-user cooldown between delete-mention packets.'),
('mentions.everyone.aliases', 'all,everyone,tutti',
'Comma-separated aliases that trigger an @everyone broadcast (requires acc_mention_everyone).'),
('mentions.friends.aliases', 'friends,amici',
'Comma-separated aliases that trigger an @friends broadcast (requires acc_mention_friends).'),
('mentions.room.aliases', 'room,stanza',
'Comma-separated aliases that trigger an @room broadcast (no permission required, room scope only).');
ALTER TABLE `wordfilter` ALTER TABLE `wordfilter`
ADD COLUMN `prefix_only` ENUM('0','1') NOT NULL DEFAULT '0' ADD COLUMN `prefix_only` ENUM('0','1') NOT NULL DEFAULT '0'
COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`; COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`;
-- ----------------------------------------------------------------------------
-- 5. Per-user mention preferences (:disablementions / :disablemassmentions)
--
-- Read by HabboStats (default '1' = enabled), toggled by the commands.
-- Without these columns the toggle commands cannot persist.
-- ----------------------------------------------------------------------------
ALTER TABLE `users_settings`
ADD COLUMN IF NOT EXISTS `mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
COMMENT 'Receive @nick mention notifications.',
ADD COLUMN IF NOT EXISTS `mass_mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
COMMENT 'Receive broadcast (@all / @friends / @room) mentions.';
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId> <groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId> <artifactId>Habbo</artifactId>
<version>4.2.31</version> <version>4.2.36</version>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -191,6 +191,8 @@ public class CommandHandler {
addCommand(new CreditsCommand()); addCommand(new CreditsCommand());
addCommand(new DanceCommand()); addCommand(new DanceCommand());
addCommand(new DiagonalCommand()); addCommand(new DiagonalCommand());
addCommand(new DisableMassMentionsCommand());
addCommand(new DisableMentionsCommand());
addCommand(new DisconnectCommand()); addCommand(new DisconnectCommand());
addCommand(new EjectAllCommand()); addCommand(new EjectAllCommand());
addCommand(new EmptyInventoryCommand()); addCommand(new EmptyInventoryCommand());
@@ -17,7 +17,22 @@ public class CommandsCommand extends Command {
message.append("(").append(commands.size()).append("):\r\n"); message.append("(").append(commands.size()).append("):\r\n");
for (Command c : commands) { for (Command c : commands) {
message.append(Emulator.getTexts().getValue("commands.description." + c.permission, "commands.description." + c.permission)).append("\r"); String textKey = "commands.description." + c.permission;
String commandText = Emulator.getTexts().getValue(textKey, "");
String commandLine = ":" + c.keys[0];
String description = "";
if (commandText.startsWith(":")) {
commandLine = commandText;
} else if (!commandText.isEmpty() && !commandText.equals(textKey)) {
description = commandText;
}
message.append(commandLine).append("\r");
if (!description.isEmpty()) {
message.append(description).append("\r");
}
} }
gameClient.getHabbo().alert(new String[]{message.toString()}); gameClient.getHabbo().alert(new String[]{message.toString()});
@@ -0,0 +1,25 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.users.Habbo;
public class DisableMassMentionsCommand extends Command {
public DisableMassMentionsCommand() {
super("cmd_disablemassmentions", new String[]{"disablemassmentions", "togglemassmentions"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (gameClient == null) return true;
Habbo habbo = gameClient.getHabbo();
if (habbo == null || habbo.getHabboStats() == null) return true;
boolean newState = !habbo.getHabboStats().massMentionsEnabled();
habbo.getHabboStats().setMassMentionsEnabled(newState);
habbo.whisper(newState
? "Broadcast mentions (@all / @friends / @room) are now ENABLED for you."
: "Broadcast mentions (@all / @friends / @room) are now DISABLED for you. Direct @nick mentions still work.");
return true;
}
}
@@ -0,0 +1,25 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.users.Habbo;
public class DisableMentionsCommand extends Command {
public DisableMentionsCommand() {
super("cmd_disablementions", new String[]{"disablementions", "togglementions"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (gameClient == null) return true;
Habbo habbo = gameClient.getHabbo();
if (habbo == null || habbo.getHabboStats() == null) return true;
boolean newState = !habbo.getHabboStats().mentionsEnabled();
habbo.getHabboStats().setMentionsEnabled(newState);
habbo.whisper(newState
? "@mention notifications are now ENABLED for you."
: "@mention notifications are now DISABLED for you. You will not receive direct or broadcast mentions.");
return true;
}
}
@@ -21,6 +21,7 @@ public class HabboMention {
private final int mentionType; private final int mentionType;
private final int timestamp; private final int timestamp;
private final boolean read; private final boolean read;
private final String senderFigure;
public HabboMention(ResultSet set) throws SQLException { public HabboMention(ResultSet set) throws SQLException {
this.id = set.getInt("id"); this.id = set.getInt("id");
@@ -33,6 +34,16 @@ public class HabboMention {
this.mentionType = set.getInt("mention_type"); this.mentionType = set.getInt("mention_type");
this.timestamp = set.getInt("timestamp"); this.timestamp = set.getInt("timestamp");
this.read = set.getInt("read") == 1; this.read = set.getInt("read") == 1;
this.senderFigure = hasSenderFigure(set) ? set.getString("sender_figure") : "";
}
private static boolean hasSenderFigure(ResultSet set) {
try {
set.findColumn("sender_figure");
return true;
} catch (SQLException e) {
return false;
}
} }
public HabboMention(int targetUserId, int id, Habbo sender, Room room, String roomName, String message, int mentionType, int timestamp) { public HabboMention(int targetUserId, int id, Habbo sender, Room room, String roomName, String message, int mentionType, int timestamp) {
@@ -46,6 +57,7 @@ public class HabboMention {
this.mentionType = mentionType; this.mentionType = mentionType;
this.timestamp = timestamp; this.timestamp = timestamp;
this.read = false; this.read = false;
this.senderFigure = sender.getHabboInfo().getLook();
} }
public int getId() { public int getId() {
@@ -87,4 +99,8 @@ public class HabboMention {
public boolean isRead() { public boolean isRead() {
return this.read; return this.read;
} }
public String getSenderFigure() {
return this.senderFigure == null ? "" : this.senderFigure;
}
} }
@@ -1,37 +1,51 @@
package com.eu.habbo.habbohotel.mentions; package com.eu.habbo.habbohotel.mentions;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.messenger.MessengerBuddy;
import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomChatType; import com.eu.habbo.habbohotel.rooms.RoomChatType;
import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboManager;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.sql.Connection; import java.sql.*;
import java.sql.PreparedStatement; import java.util.*;
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; import java.util.concurrent.ConcurrentHashMap;
public class MentionManager { public class MentionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class); private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class);
private static final int ROOM_NAME_MAX_LENGTH = 64;
private static final int MESSAGE_MAX_LENGTH = 255;
private final ConcurrentHashMap<Integer, Long> cooldowns = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Integer, Long> cooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> roomBroadcastCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> requestListCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> markReadCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> markAllCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> deleteCooldowns = new ConcurrentHashMap<>();
private volatile long lastPrune = System.currentTimeMillis();
private static final long PRUNE_INTERVAL_MS = 5 * 60_000L;
public boolean isEnabled() { public boolean isEnabled() {
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1; return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
} }
private Set<String> roomAliases() { public enum BroadcastScope {
NONE,
ROOM,
FRIENDS,
EVERYONE
}
public static final String PERMISSION_EVERYONE = "acc_mention_everyone";
public static final String PERMISSION_FRIENDS = "acc_mention_friends";
private Set<String> parseAliases(String configKey, String defaultValue) {
Set<String> aliases = new HashSet<>(); Set<String> aliases = new HashSet<>();
String raw = Emulator.getConfig().getValue("mentions.room.aliases", "amici,friends,all,everyone,tutti,room,stanza"); String raw = Emulator.getConfig().getValue(configKey, defaultValue);
for (String alias : raw.split(",")) { for (String alias : raw.split(",")) {
String trimmed = alias.trim().toLowerCase(); String trimmed = alias.trim().toLowerCase();
if (!trimmed.isEmpty()) { if (!trimmed.isEmpty()) {
@@ -41,6 +55,29 @@ public class MentionManager {
return aliases; return aliases;
} }
private Set<String> roomAliases() {
return parseAliases("mentions.room.aliases", "room,stanza");
}
private Set<String> friendsAliases() {
return parseAliases("mentions.friends.aliases", "friends,amici");
}
private Set<String> everyoneAliases() {
return parseAliases("mentions.everyone.aliases", "all,everyone,tutti");
}
private BroadcastScope classifyAlias(String alias,
Set<String> everyone,
Set<String> friends,
Set<String> room) {
if (alias.isEmpty()) return BroadcastScope.NONE;
if (everyone.contains(alias)) return BroadcastScope.EVERYONE;
if (friends.contains(alias)) return BroadcastScope.FRIENDS;
if (room.contains(alias)) return BroadcastScope.ROOM;
return BroadcastScope.NONE;
}
public void process(Habbo sender, Room room, String message, RoomChatType type) { public void process(Habbo sender, Room room, String message, RoomChatType type) {
try { try {
if (!this.isEnabled()) { if (!this.isEnabled()) {
@@ -63,8 +100,11 @@ public class MentionManager {
return; return;
} }
Set<String> aliases = this.roomAliases(); Set<String> roomAliases = this.roomAliases();
boolean roomBroadcast = false; Set<String> friendsAliases = this.friendsAliases();
Set<String> everyoneAliases = this.everyoneAliases();
BroadcastScope broadcastScope = BroadcastScope.NONE;
LinkedHashSet<String> directTokens = new LinkedHashSet<>(); LinkedHashSet<String> directTokens = new LinkedHashSet<>();
for (String token : message.split("\\s+")) { for (String token : message.split("\\s+")) {
@@ -75,47 +115,72 @@ public class MentionManager {
String raw = token.substring(1); String raw = token.substring(1);
String aliasCandidate = trimTrailingPunctuation(raw).toLowerCase(); String aliasCandidate = trimTrailingPunctuation(raw).toLowerCase();
if (!aliasCandidate.isEmpty() && aliases.contains(aliasCandidate)) { BroadcastScope scope = this.classifyAlias(aliasCandidate, everyoneAliases, friendsAliases, roomAliases);
roomBroadcast = true;
if (scope != BroadcastScope.NONE) {
if (scope.ordinal() > broadcastScope.ordinal()) {
broadcastScope = scope;
}
} else if (!raw.isEmpty()) { } else if (!raw.isEmpty()) {
directTokens.add(raw); directTokens.add(raw);
} }
} }
if (!roomBroadcast && directTokens.isEmpty()) { if (broadcastScope == BroadcastScope.EVERYONE && !sender.hasPermission(PERMISSION_EVERYONE)) {
broadcastScope = BroadcastScope.NONE;
} else if (broadcastScope == BroadcastScope.FRIENDS && !sender.hasPermission(PERMISSION_FRIENDS)) {
broadcastScope = BroadcastScope.NONE;
}
if (broadcastScope == BroadcastScope.NONE && directTokens.isEmpty()) {
return; return;
} }
if (broadcastScope != BroadcastScope.NONE) {
long roomCooldownMs = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
Long lastRoom = this.roomBroadcastCooldowns.get(senderId);
if (lastRoom != null && (now - lastRoom) < roomCooldownMs) {
return;
}
}
int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50); int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50);
if (maxTargets <= 0) maxTargets = 1;
int maxDirectTokens = Math.min(directTokens.size(), maxTargets);
List<Habbo> targets = new ArrayList<>(); List<Habbo> targets = new ArrayList<>();
Set<Integer> seen = new HashSet<>(); Set<Integer> seen = new HashSet<>();
if (roomBroadcast) { switch (broadcastScope) {
for (Habbo habbo : room.getHabbos()) { case EVERYONE:
if (habbo == null || habbo.getHabboInfo().getId() == senderId) { this.collectEveryoneTargets(senderId, targets, seen, maxTargets);
continue; break;
case FRIENDS:
this.collectFriendsTargets(sender, senderId, targets, seen, maxTargets);
break;
case ROOM:
this.collectRoomTargets(room, senderId, targets, seen, maxTargets, true);
break;
case NONE:
default:
int processed = 0;
for (String token : directTokens) {
if (processed++ >= maxDirectTokens) break;
Habbo habbo = this.resolveHabbo(room, token);
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
continue;
}
if (!acceptsMention(habbo, false)) {
continue;
}
if (seen.add(habbo.getHabboInfo().getId())) {
targets.add(habbo);
}
if (targets.size() >= maxTargets) {
break;
}
} }
if (seen.add(habbo.getHabboInfo().getId())) { break;
targets.add(habbo);
}
if (targets.size() >= maxTargets) {
break;
}
}
} else {
for (String token : directTokens) {
Habbo habbo = this.resolveHabbo(room, token);
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
continue;
}
if (seen.add(habbo.getHabboInfo().getId())) {
targets.add(habbo);
}
if (targets.size() >= maxTargets) {
break;
}
}
} }
if (targets.isEmpty()) { if (targets.isEmpty()) {
@@ -123,15 +188,13 @@ public class MentionManager {
} }
this.cooldowns.put(senderId, now); this.cooldowns.put(senderId, now);
if (broadcastScope != BroadcastScope.NONE) this.roomBroadcastCooldowns.put(senderId, now);
this.pruneCooldownsIfDue(now);
int mentionType = roomBroadcast ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT; int mentionType = (broadcastScope != BroadcastScope.NONE) ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT;
int timestamp = Emulator.getIntUnixTimestamp(); int timestamp = Emulator.getIntUnixTimestamp();
String roomName = room.getName(); String roomName = truncate(room.getName(), ROOM_NAME_MAX_LENGTH);
String storedMessage = truncate(message, MESSAGE_MAX_LENGTH);
String storedMessage = message;
if (storedMessage.length() > 255) {
storedMessage = storedMessage.substring(0, 255);
}
for (Habbo target : targets) { for (Habbo target : targets) {
this.store(target, sender, room, storedMessage, mentionType, timestamp, roomName); this.store(target, sender, room, storedMessage, mentionType, timestamp, roomName);
@@ -141,6 +204,48 @@ public class MentionManager {
} }
} }
private void collectRoomTargets(Room room, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets, boolean isBroadcast) {
for (Habbo habbo : room.getHabbos()) {
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
if (!acceptsMention(habbo, isBroadcast)) continue;
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
if (targets.size() >= maxTargets) break;
}
}
private void collectFriendsTargets(Habbo sender, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
if (sender.getMessenger() == null) return;
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
for (MessengerBuddy buddy : sender.getMessenger().getFriends().values()) {
if (buddy == null) continue;
int buddyId = buddy.getId();
if (buddyId == senderId) continue;
Habbo online = habboManager.getHabbo(buddyId);
if (online == null) continue;
if (!acceptsMention(online, true)) continue;
if (seen.add(buddyId)) targets.add(online);
if (targets.size() >= maxTargets) break;
}
}
private void collectEveryoneTargets(int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
if (!acceptsMention(habbo, true)) continue;
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
if (targets.size() >= maxTargets) break;
}
}
private boolean acceptsMention(Habbo recipient, boolean isBroadcast) {
if (recipient == null) return false;
if (recipient.getClient() == null) return false;
if (recipient.getHabboStats() == null) return false;
if (!recipient.getHabboStats().mentionsEnabled()) return false;
if (isBroadcast && !recipient.getHabboStats().massMentionsEnabled()) return false;
return true;
}
private void store(Habbo target, Habbo sender, Room room, String message, int mentionType, int timestamp, String roomName) { private void store(Habbo target, Habbo sender, Room room, String message, int mentionType, int timestamp, String roomName) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( PreparedStatement statement = connection.prepareStatement(
@@ -163,6 +268,10 @@ public class MentionManager {
} }
} }
if (generatedId <= 0) {
return;
}
HabboMention mention = new HabboMention(target.getHabboInfo().getId(), generatedId, sender, room, roomName, message, mentionType, timestamp); HabboMention mention = new HabboMention(target.getHabboInfo().getId(), generatedId, sender, room, roomName, message, mentionType, timestamp);
if (target.getClient() != null) { if (target.getClient() != null) {
@@ -175,9 +284,11 @@ public class MentionManager {
public List<HabboMention> getMentions(int userId, int limit) { public List<HabboMention> getMentions(int userId, int limit) {
List<HabboMention> mentions = new ArrayList<>(); List<HabboMention> mentions = new ArrayList<>();
if (limit <= 0) limit = 50;
if (limit > 200) limit = 200;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM habbo_mentions WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) { "SELECT habbo_mentions.*, users.look AS sender_figure FROM habbo_mentions LEFT JOIN users ON users.id = habbo_mentions.sender_user_id WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, userId); statement.setInt(1, userId);
statement.setInt(2, limit); statement.setInt(2, limit);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
@@ -192,9 +303,12 @@ public class MentionManager {
} }
public void markRead(int userId, int mode, int mentionId) { public void markRead(int userId, int mode, int mentionId) {
if (mode != 0 && mode != 1) return;
if (mode == 1 && mentionId <= 0) return;
String query = mode == 1 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 = ? AND id = ? AND `read` = 0"
: "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ?"; : "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND `read` = 0";
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(query)) { PreparedStatement statement = connection.prepareStatement(query)) {
statement.setInt(1, userId); statement.setInt(1, userId);
@@ -208,6 +322,7 @@ public class MentionManager {
} }
public void delete(int userId, int mentionId) { public void delete(int userId, int mentionId) {
if (mentionId <= 0) return;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( PreparedStatement statement = connection.prepareStatement(
"DELETE FROM habbo_mentions WHERE target_user_id = ? AND id = ?")) { "DELETE FROM habbo_mentions WHERE target_user_id = ? AND id = ?")) {
@@ -219,6 +334,70 @@ public class MentionManager {
} }
} }
public boolean tryAcquireRequestList(int userId) {
long cooldownMs = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
return tryAcquire(this.requestListCooldowns, userId, cooldownMs);
}
public boolean tryAcquireMarkRead(int userId, int mode) {
long cooldownMs;
ConcurrentHashMap<Integer, Long> bucket;
if (mode == 1) {
cooldownMs = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
bucket = this.markReadCooldowns;
} else {
cooldownMs = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
bucket = this.markAllCooldowns;
}
return tryAcquire(bucket, userId, cooldownMs);
}
public boolean tryAcquireDelete(int userId) {
long cooldownMs = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
return tryAcquire(this.deleteCooldowns, userId, cooldownMs);
}
private boolean tryAcquire(ConcurrentHashMap<Integer, Long> bucket, int userId, long cooldownMs) {
long now = System.currentTimeMillis();
Long last = bucket.get(userId);
if (last != null && (now - last) < cooldownMs) {
return false;
}
bucket.put(userId, now);
this.pruneCooldownsIfDue(now);
return true;
}
private void pruneCooldownsIfDue(long now) {
if (now - this.lastPrune < PRUNE_INTERVAL_MS) return;
this.lastPrune = now;
long mentionWindow = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
long roomWindow = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
long requestWindow = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
long markReadWindow = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
long markAllWindow = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
long deleteWindow = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
prune(this.cooldowns, now, mentionWindow);
prune(this.roomBroadcastCooldowns, now, roomWindow);
prune(this.requestListCooldowns, now, requestWindow);
prune(this.markReadCooldowns, now, markReadWindow);
prune(this.markAllCooldowns, now, markAllWindow);
prune(this.deleteCooldowns, now, deleteWindow);
}
private static void prune(ConcurrentHashMap<Integer, Long> bucket, long now, long windowMs) {
Iterator<Map.Entry<Integer, Long>> it = bucket.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Long> entry = it.next();
Long value = entry.getValue();
if (value == null || (now - value) >= windowMs) {
it.remove();
}
}
}
private static final String TRAILING_PUNCTUATION = ".,!?;:)]}\"'"; private static final String TRAILING_PUNCTUATION = ".,!?;:)]}\"'";
private static String trimTrailingPunctuation(String value) { private static String trimTrailingPunctuation(String value) {
@@ -229,20 +408,55 @@ public class MentionManager {
return value.substring(0, end); return value.substring(0, end);
} }
/** private static String truncate(String value, int max) {
* Resolve a present room occupant from a raw mention token. Tries the token if (value == null) return "";
* verbatim first (so usernames containing allowed punctuation such as '-', if (value.length() <= max) return value;
* '.', '!' still match), then falls back to a trailing-punctuation-trimmed return value.substring(0, max);
* form so a mention written as "@Bob!" still resolves the user "Bob". }
*/
private boolean isBotOrPetName(Room room, String token) {
if (room == null || token == null || token.isEmpty()) return false;
List<com.eu.habbo.habbohotel.bots.Bot> bots = room.getBots(token);
if (bots != null && !bots.isEmpty()) return true;
if (room.getUnitManager() != null && room.getUnitManager().getPets() != null) {
for (com.eu.habbo.habbohotel.pets.Pet pet : room.getUnitManager().getPets()) {
if (pet != null && pet.getName() != null && pet.getName().equalsIgnoreCase(token)) {
return true;
}
}
}
return false;
}
private Habbo resolveHabbo(Room room, String rawToken) { private Habbo resolveHabbo(Room room, String rawToken) {
if (isBotOrPetName(room, rawToken)) {
return null;
}
String trimmedForBotCheck = trimTrailingPunctuation(rawToken);
if (!trimmedForBotCheck.equals(rawToken) && isBotOrPetName(room, trimmedForBotCheck)) {
return null;
}
Habbo habbo = room.getHabbo(rawToken); Habbo habbo = room.getHabbo(rawToken);
if (habbo != null) { if (habbo != null) {
return habbo; return habbo;
} }
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
habbo = habboManager.getHabbo(rawToken);
if (habbo != null) {
return habbo;
}
String trimmed = trimTrailingPunctuation(rawToken); String trimmed = trimTrailingPunctuation(rawToken);
if (!trimmed.isEmpty() && !trimmed.equals(rawToken)) { if (!trimmed.isEmpty() && !trimmed.equals(rawToken)) {
return room.getHabbo(trimmed); habbo = room.getHabbo(trimmed);
if (habbo != null) {
return habbo;
}
return habboManager.getHabbo(trimmed);
} }
return null; return null;
} }
@@ -94,6 +94,8 @@ public class HabboStats implements Runnable {
public boolean hasGottenDefaultSavedSearches; public boolean hasGottenDefaultSavedSearches;
private HabboInfo habboInfo; private HabboInfo habboInfo;
private boolean allowTrade; private boolean allowTrade;
private boolean mentionsEnabled;
private boolean massMentionsEnabled;
private int clubExpireTimestamp; private int clubExpireTimestamp;
private int muteEndTime; private int muteEndTime;
public int maxFriends; public int maxFriends;
@@ -131,6 +133,8 @@ public class HabboStats implements Runnable {
this.guilds = new ArrayList<>(); this.guilds = new ArrayList<>();
this.tags = set.getString("tags").split(";"); this.tags = set.getString("tags").split(";");
this.allowTrade = set.getString("can_trade").equals("1"); this.allowTrade = set.getString("can_trade").equals("1");
this.mentionsEnabled = "1".equals(safeColumnString(set, "mentions_enabled", "1"));
this.massMentionsEnabled = "1".equals(safeColumnString(set, "mass_mentions_enabled", "1"));
this.votedRooms = new TIntArrayStack(); this.votedRooms = new TIntArrayStack();
this.clubExpireTimestamp = set.getInt("club_expire_timestamp"); this.clubExpireTimestamp = set.getInt("club_expire_timestamp");
this.loginStreak = set.getInt("login_streak"); this.loginStreak = set.getInt("login_streak");
@@ -749,13 +753,6 @@ public class HabboStats implements Runnable {
return 0; return 0;
} }
/**
* Ignore an user.
*
* @param gameClient The client to which this HabboStats instance belongs.
* @param userId The user to ignore.
* @return true if successfully ignored, false otherwise.
*/
public boolean ignoreUser(GameClient gameClient, int userId) { public boolean ignoreUser(GameClient gameClient, int userId) {
final Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); final Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
@@ -805,6 +802,44 @@ public class HabboStats implements Runnable {
else return this.allowTrade; else return this.allowTrade;
} }
public boolean mentionsEnabled() {
return this.mentionsEnabled;
}
public boolean massMentionsEnabled() {
return this.massMentionsEnabled;
}
public void setMentionsEnabled(boolean enabled) {
this.mentionsEnabled = enabled;
persistFlag("mentions_enabled", enabled);
}
public void setMassMentionsEnabled(boolean enabled) {
this.massMentionsEnabled = enabled;
persistFlag("mass_mentions_enabled", enabled);
}
private void persistFlag(String column, boolean enabled) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET `" + column + "` = ? WHERE user_id = ? LIMIT 1")) {
statement.setString(1, enabled ? "1" : "0");
statement.setInt(2, this.habboInfo.getId());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to persist users_settings.{} for user {}", column, this.habboInfo.getId(), e);
}
}
private static String safeColumnString(ResultSet set, String column, String defaultValue) {
try {
String value = set.getString(column);
return value == null ? defaultValue : value;
} catch (SQLException e) {
return defaultValue;
}
}
public void setAllowTrade(boolean allowTrade) { public void setAllowTrade(boolean allowTrade) {
this.allowTrade = allowTrade; this.allowTrade = allowTrade;
} }
@@ -1,14 +1,27 @@
package com.eu.habbo.messages.incoming.mentions; package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
public class DeleteMentionEvent extends MessageHandler { public class DeleteMentionEvent extends MessageHandler {
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
if (this.client == null || this.client.getHabbo() == null) return;
int userId = this.client.getHabbo().getHabboInfo().getId(); int userId = this.client.getHabbo().getHabboInfo().getId();
int mentionId = this.packet.readInt(); int mentionId = this.packet.readInt();
Emulator.getGameEnvironment().getMentionManager().delete(userId, mentionId); if (mentionId <= 0) {
return;
}
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
if (!manager.tryAcquireDelete(userId)) {
return;
}
manager.delete(userId, mentionId);
} }
} }
@@ -1,15 +1,35 @@
package com.eu.habbo.messages.incoming.mentions; package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
public class MarkMentionsReadEvent extends MessageHandler { public class MarkMentionsReadEvent extends MessageHandler {
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
if (this.client == null || this.client.getHabbo() == null) return;
int userId = this.client.getHabbo().getHabboInfo().getId(); int userId = this.client.getHabbo().getHabboInfo().getId();
int mode = this.packet.readInt(); int mode = this.packet.readInt();
int mentionId = this.packet.readInt(); int mentionId = this.packet.readInt();
Emulator.getGameEnvironment().getMentionManager().markRead(userId, mode, mentionId); // Only mode 0 (mark-all) and mode 1 (mark-single) are valid. Reject
// anything else so a crafted packet can't fall into the mark-all branch
// by accident.
if (mode != 0 && mode != 1) {
return;
}
if (mode == 1 && mentionId <= 0) {
return;
}
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
if (!manager.tryAcquireMarkRead(userId, mode)) {
return;
}
manager.markRead(userId, mode, mentionId);
} }
} }
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.mentions.HabboMention; import com.eu.habbo.habbohotel.mentions.HabboMention;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.mentions.MentionsListComposer; import com.eu.habbo.messages.outgoing.mentions.MentionsListComposer;
@@ -10,10 +11,19 @@ import java.util.List;
public class RequestMentionsEvent extends MessageHandler { public class RequestMentionsEvent extends MessageHandler {
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
if (this.client == null || this.client.getHabbo() == null) return;
int userId = this.client.getHabbo().getHabboInfo().getId(); int userId = this.client.getHabbo().getHabboInfo().getId();
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
if (!manager.tryAcquireRequestList(userId)) {
return;
}
int limit = Emulator.getConfig().getInt("mentions.store.limit", 50); int limit = Emulator.getConfig().getInt("mentions.store.limit", 50);
List<HabboMention> mentions = Emulator.getGameEnvironment().getMentionManager().getMentions(userId, limit); List<HabboMention> mentions = manager.getMentions(userId, limit);
this.client.sendResponse(new MentionsListComposer(mentions)); this.client.sendResponse(new MentionsListComposer(mentions));
} }
} }
@@ -18,6 +18,7 @@ public class MentionReceivedComposer extends MessageComposer {
this.response.appendInt(this.mention.getId()); this.response.appendInt(this.mention.getId());
this.response.appendInt(this.mention.getSenderUserId()); this.response.appendInt(this.mention.getSenderUserId());
this.response.appendString(this.mention.getSenderUsername()); this.response.appendString(this.mention.getSenderUsername());
this.response.appendString(this.mention.getSenderFigure());
this.response.appendInt(this.mention.getRoomId()); this.response.appendInt(this.mention.getRoomId());
this.response.appendString(this.mention.getRoomName()); this.response.appendString(this.mention.getRoomName());
this.response.appendString(this.mention.getMessage()); this.response.appendString(this.mention.getMessage());
@@ -23,6 +23,7 @@ public class MentionsListComposer extends MessageComposer {
this.response.appendInt(mention.getId()); this.response.appendInt(mention.getId());
this.response.appendInt(mention.getSenderUserId()); this.response.appendInt(mention.getSenderUserId());
this.response.appendString(mention.getSenderUsername()); this.response.appendString(mention.getSenderUsername());
this.response.appendString(mention.getSenderFigure());
this.response.appendInt(mention.getRoomId()); this.response.appendInt(mention.getRoomId());
this.response.appendString(mention.getRoomName()); this.response.appendString(mention.getRoomName());
this.response.appendString(mention.getMessage()); this.response.appendString(mention.getMessage());
+1 -1
View File
@@ -10,7 +10,7 @@ and is developed for free by talented developers and is compatible with the foll
[Latest compiled version](https://github.com/duckietm/Arcturus-Morningstar-Extended/tree/main/Latest_Compiled_Version) [Latest compiled version](https://github.com/duckietm/Arcturus-Morningstar-Extended/tree/main/Latest_Compiled_Version)
## Connection ## ## Connection ##
Use the Websocket plugin! Use the BUILD-IN Websocket so do NOT load any websocket plugin!
### How do I connect to my emulator using Secure Websockets (wss)? ### How do I connect to my emulator using Secure Websockets (wss)?
You have several options to add WSS support to your websocket server. You have several options to add WSS support to your websocket server.