🆕 Frank the BOT

-- VERY IMPORTANT !!!!
-- First check if the items_base ID and catalog_items ID is not in use !
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
This commit is contained in:
duckietm
2026-05-28 10:41:25 +02:00
parent 539c5b5b96
commit bab43af41e
7 changed files with 585 additions and 3 deletions
+70
View File
@@ -0,0 +1,70 @@
ALTER TABLE `bots`
MODIFY COLUMN `type` ENUM('generic','visitor_log','bartender','weapons_dealer','frank')
NOT NULL DEFAULT 'generic';
INSERT INTO `permission_definitions`
(`permission_key`, `max_value`, `comment`,
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
VALUES
('acc_bot_frank', 1, 'Required to purchase the Frank mascot bot from the catalog.',
0, 0, 0, 0, 0, 0, 1)
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
CREATE TABLE IF NOT EXISTS `bot_chat_responses` (
`id` INT NOT NULL AUTO_INCREMENT,
`bot_type` VARCHAR(32) NOT NULL,
`keys` VARCHAR(255) NOT NULL COMMENT 'semicolon-separated trigger words',
`responses` TEXT NOT NULL COMMENT 'newline-separated replies; bot picks one at random',
PRIMARY KEY (`id`),
KEY `bot_type` (`bot_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
('frank', '__door_triggers', 'show me the door\nkick me\ni want to leave\nlet me out'),
('frank', '__door_lines', 'Right this way - mind the step!\nAnd out you go. Come back soon!\nAllow me to escort you to the exit.\nThere''s the door. Farewell, true believer!'),
('frank', '__busy_whisper', 'Sorry, I am currently busy. Please wait until I am available.'),
('frank', 'frank', 'Hello, I''m Frank! Welcome to Habbo.'),
('frank', 'help', 'What do you need help with?'),
('frank', 'thanks;thank you', 'Just doing my job, true believer!'),
('frank', 'new', 'Welcome to Habbo! I hope you have a great time here.'),
('frank', 'rooms', 'Looking for somewhere fun? Try the Navigator - thousands of rooms to explore!'),
('frank', 'sulake', 'Sulake is the company behind Habbo. Take a look: https://www.sulake.com'),
('frank', 'vip;hc', 'VIP gets you more outfits, more furni, more everything. Worth it!'),
('frank', 'music', 'Snoop Dogg, Frank Sinatra and a little Beethoven on Sundays.'),
('frank', 'movie', 'I''m a Casablanca man. Black and white films are an underrated art.'),
('frank', 'game', 'Battleship. Always Battleship.'),
('frank', 'snowstorm', 'Honestly? I''m terrible at Snowstorm. Don''t tell anyone.'),
('frank', 'furni', 'Best furniture maker in town - hands down, the folks at Sulake.'),
('frank', 'animal;cat;pet','I have a cat called Mr. Whiskers. He runs the place, really.'),
('frank', 'miranda', 'Miranda. The love of my life. Don''t get me started.'),
('frank', 'frank black', 'Named after the man himself. Frank Black is a hero of mine.'),
('frank', 'life', 'Life is like a bowl of popcorn - warm, salty and buttery.'),
('frank', 'job;work', 'I''m sure you can find work in one of the guest rooms!'),
('frank', 'snouthill', 'Snouthill... so many memories.'),
('frank', 'wife', 'I had a wife once. She broke my stereo.'),
('frank', 'baseball', 'Oh, I used to love to go down to the old ball park and watch Christy Mathewson and Honus Wagner at bat.'),
('frank', 'mark', 'I don''t trust Mark.'),
('frank', 'vietnam', 'Vietnam? Don''t ask. Worst trip of my life.'),
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
('sunflower', 21),
('cola;habbo cola', 32),
('rose', 1000),
('book', 20),
('tea', 6),
('coffee', 1),
('migraine;headache;pills', 34),
('radioactive liquid;radioactive', 36),
('turkey;can of turkey', 38);
-- VERY IMPORTANT !!!!
-- First check if the items_base ID and catalog_items ID is not in use !
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
INSERT IGNORE INTO `items_base` (`id`, `sprite_id`, `item_name`, `public_name`, `width`, `length`, `stack_height`, `allow_stack`, `allow_sit`, `allow_lay`, `allow_walk`, `allow_gift`, `allow_trade`, `allow_recycle`, `allow_marketplace_sell`, `allow_inventory_stack`, `type`, `interaction_type`, `interaction_modes_count`, `vending_ids`, `multiheight`, `customparams`)
VALUES (99001, 0, 'bot_frank', 'Frank', 1, 1, 0.00, '0', '0', '0', '1', '0', '0', '0', '0', '0', 's', 'default', 1, '0', '0', 'name:Frank;motto:Welcome to Habbo!;figure:hr-3499-33.sh-290-90.ch-3971-72-73.lg-270-73.hd-205-1-1.fa-1206-67.ha-3409-73-72;gender:M');
INSERT IGNORE INTO `catalog_items` (`item_ids`, `page_id`, `offer_id`, `catalog_name`, `cost_credits`, `cost_points`, `points_type`, `amount`, `extradata`)
VALUES ('99001', 1, 99001, 'Frank', 0, 0, 0, 1, '0');
@@ -41,6 +41,7 @@ public class BotManager {
addBotDefinition("generic", Bot.class); addBotDefinition("generic", Bot.class);
addBotDefinition("bartender", ButlerBot.class); addBotDefinition("bartender", ButlerBot.class);
addBotDefinition("visitor_log", VisitorBot.class); addBotDefinition("visitor_log", VisitorBot.class);
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
this.reload(); this.reload();
@@ -0,0 +1,475 @@
package com.eu.habbo.habbohotel.bots;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomChatMessage;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.rooms.RoomUserRotation;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer;
import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
/**
* Frank, Habbo's mascot. Behaves as a Butler (inherits the give-item
* walk-and-serve flow from ButlerBot via bot_serves) plus a keyword
* to chat-reply layer for the non-item conversational commands
* ("cat", "Vietnam", "Miranda", "help", "Frank Black", "life",
* etc).
*
* Chat replies live in `bot_chat_responses` keyed by bot_type so
* other future scripted bots can reuse the same table. Each row's
* `keys` is a semicolon-separated list of trigger words, `responses`
* is a newline-separated list of replies that Frank picks from at
* random for variety.
*
* Resolution order on each user message: chat reply first (instant,
* no walking) - if no chat keyword hits, fall through to the
* inherited ButlerBot logic so item commands keep working.
*/
public class FrankBot extends ButlerBot {
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
public static final String BOT_TYPE = "frank";
public static final String PERMISSION_USE = "acc_bot_frank";
private static final String KEY_DOOR_LINES = "__door_lines";
private static final String KEY_BUSY_WHISPER = "__busy_whisper";
private static final String KEY_DOOR_TRIGGERS = "__door_triggers";
// Fallbacks only - the live values are loaded from bot_chat_responses
// and surface to admins via the __door_lines / __busy_whisper /
// __door_triggers rows. These constants are used only if the SQL row
// is missing so Frank does not break on a bad install.
private static final List<String> DEFAULT_DOOR_LINES = List.of(
"Right this way - mind the step!",
"And out you go. Come back soon!",
"Allow me to escort you to the exit.",
"There's the door. Farewell, true believer!"
);
private static final String DEFAULT_BUSY_WHISPER =
"Sorry, I am currently busy. Please wait until I am available.";
private static final Pattern DEFAULT_DOOR_PATTERN = Pattern.compile(
"\\b(show me the door|kick me|i want to leave|let me out)\\b");
private static final ConcurrentHashMap<Pattern, List<String>> chatResponses = new ConcurrentHashMap<>();
private static volatile List<String> doorLines = DEFAULT_DOOR_LINES;
private static volatile String busyWhisper = DEFAULT_BUSY_WHISPER;
private static volatile Pattern doorTriggerPattern = DEFAULT_DOOR_PATTERN;
private static final Random RANDOM = new Random();
private static final int MAX_CHAT_KEYWORDS = 256;
private static final int MAX_DOOR_TRIGGERS = 32;
private static final int MAX_MESSAGE_LEN = 256;
private static final long BUSY_WHISPER_COOLDOWN_MS = 5000L;
private volatile RoomTile homeTile;
private volatile RoomUserRotation homeRotation;
private final AtomicBoolean busy = new AtomicBoolean(false);
private final AtomicBoolean returnScheduled = new AtomicBoolean(false);
private final ConcurrentHashMap<Integer, Long> lastBusyWhisperAt = new ConcurrentHashMap<>();
public FrankBot(ResultSet set) throws SQLException {
super(set);
}
public FrankBot(Bot bot) {
super(bot);
}
@Override
public void onPlace(Habbo habbo, Room room) {
super.onPlace(habbo, room);
if (this.getRoomUnit() != null) {
this.homeTile = this.getRoomUnit().getCurrentLocation();
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
}
public static void initialise() {
chatResponses.clear();
doorLines = DEFAULT_DOOR_LINES;
busyWhisper = DEFAULT_BUSY_WHISPER;
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
Statement statement = connection.createStatement();
ResultSet set = statement.executeQuery("SELECT `keys`, `responses` FROM bot_chat_responses WHERE bot_type = '" + BOT_TYPE + "'")) {
while (set.next()) {
String keysRaw = set.getString("keys");
String responsesRaw = set.getString("responses");
if (keysRaw == null || responsesRaw == null) continue;
List<String> responses = new ArrayList<>();
for (String line : responsesRaw.split("\n")) {
String trimmed = line.trim();
if (!trimmed.isEmpty()) responses.add(trimmed);
}
if (responses.isEmpty()) continue;
String firstKey = keysRaw.split(";", 2)[0].trim();
if (firstKey.startsWith("__")) {
switch (firstKey) {
case KEY_DOOR_LINES:
doorLines = new CopyOnWriteArrayList<>(responses);
break;
case KEY_BUSY_WHISPER:
busyWhisper = responses.get(0);
break;
case KEY_DOOR_TRIGGERS:
doorTriggerPattern = buildDoorTriggerPattern(responses);
break;
default:
LOGGER.warn("FrankBot: unknown system key '{}', ignored", firstKey);
}
continue;
}
List<String> shared = new CopyOnWriteArrayList<>(responses);
for (String key : keysRaw.split(";")) {
if (chatResponses.size() >= MAX_CHAT_KEYWORDS) {
LOGGER.warn("FrankBot: chat keyword cap ({}) reached, remaining rows ignored",
MAX_CHAT_KEYWORDS);
break;
}
String k = key == null ? "" : key.trim().toLowerCase();
if (k.isEmpty()) continue;
try {
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(k) + "\\b");
chatResponses.put(pattern, shared);
} catch (Exception e) {
LOGGER.error("Failed to compile Frank chat keyword pattern: {}", k, e);
}
}
}
} catch (SQLException e) {
// Table missing or transient DB issue - log once and let the bot
// fall through to ButlerBot behavior. The chat layer is additive.
LOGGER.warn("FrankBot: could not load bot_chat_responses ({}). Frank will still serve items.", e.getMessage());
}
ButlerBot.initialise();
}
public static void dispose() {
chatResponses.clear();
doorLines = DEFAULT_DOOR_LINES;
busyWhisper = DEFAULT_BUSY_WHISPER;
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
ButlerBot.dispose();
}
private static Pattern buildDoorTriggerPattern(List<String> triggers) {
StringBuilder sb = new StringBuilder("\\b(");
boolean first = true;
int count = 0;
for (String trigger : triggers) {
if (count >= MAX_DOOR_TRIGGERS) {
LOGGER.warn("FrankBot: door trigger cap ({}) reached, extra entries ignored",
MAX_DOOR_TRIGGERS);
break;
}
String t = trigger == null ? "" : trigger.trim().toLowerCase();
if (t.isEmpty()) continue;
if (!first) sb.append('|');
sb.append(Pattern.quote(t));
first = false;
count++;
}
sb.append(")\\b");
if (first) return DEFAULT_DOOR_PATTERN;
try {
return Pattern.compile(sb.toString());
} catch (Exception e) {
LOGGER.error("FrankBot: failed to compile door trigger pattern from {}, falling back to default", triggers, e);
return DEFAULT_DOOR_PATTERN;
}
}
@Override
public void onUserSay(final RoomChatMessage message) {
Room currentRoom = this.getRoom();
if (currentRoom == null) return;
Habbo asker = message.getHabbo();
if (asker == null || asker.getClient() == null) return;
if (this.getRoomUnit() == null) return;
String raw = message.getUnfilteredMessage();
if (raw != null && raw.length() > MAX_MESSAGE_LEN) return;
if (this.homeTile == null) {
this.homeTile = this.getRoomUnit().getCurrentLocation();
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
if (this.busy.get() || this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
if (raw != null) {
double distance = this.getRoomUnit().getCurrentLocation().distance(asker.getRoomUnit().getCurrentLocation());
int commandDistance = Emulator.getConfig().getInt("hotel.bot.butler.commanddistance");
if (distance <= commandDistance) {
String lower = raw.toLowerCase();
if (doorTriggerPattern.matcher(lower).find()) {
if (!this.busy.compareAndSet(false, true)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
this.showToTheDoor(asker);
return;
}
for (java.util.Map.Entry<Pattern, List<String>> entry : chatResponses.entrySet()) {
if (entry.getKey().matcher(lower).find()) {
List<String> options = entry.getValue();
if (options.isEmpty()) continue;
String reply = options.get(RANDOM.nextInt(options.size()));
this.talk(reply);
return;
}
}
}
}
if (!this.busy.compareAndSet(false, true)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
super.onUserSay(message);
this.schedulePostServeReturn(currentRoom.getId(), 0);
}
private void whisperThrottled(Habbo target, String text) {
if (target == null || text == null || text.isEmpty() || this.getRoomUnit() == null) return;
int userId = target.getHabboInfo().getId();
long now = System.currentTimeMillis();
Long last = lastBusyWhisperAt.get(userId);
if (last != null && (now - last) < BUSY_WHISPER_COOLDOWN_MS) return;
lastBusyWhisperAt.put(userId, now);
RoomChatMessage msg = new RoomChatMessage(text, this.getRoomUnit(), RoomChatMessageBubbles.BOT);
target.getClient().sendResponse(new RoomUserWhisperComposer(msg));
}
private void showToTheDoor(final Habbo target) {
final Room room = this.getRoom();
if (room == null || room.getLayout() == null || target == null) {
this.busy.set(false);
return;
}
final RoomTile doorTile = room.getLayout().getDoorTile();
if (doorTile == null) {
this.busy.set(false);
return;
}
this.lookAt(target);
List<String> lines = doorLines;
String line = lines.isEmpty() ? DEFAULT_DOOR_LINES.get(RANDOM.nextInt(DEFAULT_DOOR_LINES.size()))
: lines.get(RANDOM.nextInt(lines.size()));
this.talk(line);
final int targetId = target.getHabboInfo().getId();
final int roomId = room.getId();
final AtomicBoolean fired = new AtomicBoolean(false);
final Runnable kickThenReturn = () -> {
if (!fired.compareAndSet(false, true)) return;
Room currentRoom = this.getRoom();
if (currentRoom == null || currentRoom.getId() != roomId) {
this.busy.set(false);
return;
}
Habbo stillHere = currentRoom.getHabbo(targetId);
if (stillHere != null) {
currentRoom.kickHabbo(stillHere, false);
}
this.scheduleReturnHome(targetId, roomId, 0);
};
if (this.getRoomUnit().canWalk() && !this.getRoomUnit().getCurrentLocation().equals(doorTile)) {
List<Runnable> onArrive = new ArrayList<>();
onArrive.add(kickThenReturn);
List<Runnable> onFail = new ArrayList<>();
onFail.add(() -> Emulator.getThreading().run(kickThenReturn, 1500));
this.getRoomUnit().setGoalLocation(doorTile);
Emulator.getThreading().run(
new RoomUnitWalkToLocation(this.getRoomUnit(), doorTile, room, onArrive, onFail));
} else {
Emulator.getThreading().run(kickThenReturn, 1500);
}
}
private static final int RETURN_HOME_POLL_MS = 500;
private static final int RETURN_HOME_MAX_WAIT_MS = 8000;
private static final int POST_SERVE_POLL_MS = 750;
private static final int POST_SERVE_MAX_WAIT_MS = 30000;
private void schedulePostServeReturn(final int roomId, final int waitedMs) {
if (waitedMs == 0 && !this.returnScheduled.compareAndSet(false, true)) {
return;
}
if (waitedMs >= POST_SERVE_MAX_WAIT_MS) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
if (this.homeTile == null) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
Emulator.getThreading().run(() -> {
Room r = this.getRoom();
if (r == null || r.getId() != roomId || this.getRoomUnit() == null || this.homeTile == null) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
if (this.homeRotation != null && this.getRoomUnit().getBodyRotation() != this.homeRotation) {
this.getRoomUnit().setRotation(this.homeRotation);
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
this.persistPosition();
} else {
this.busy.set(false);
}
this.returnScheduled.set(false);
return;
}
boolean stillWalking = this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)
|| (this.getRoomUnit().getPath() != null && !this.getRoomUnit().getPath().isEmpty());
if (stillWalking) {
this.schedulePostServeReturn(roomId, waitedMs + POST_SERVE_POLL_MS);
return;
}
this.returnScheduled.set(false);
this.returnHome(-1, false);
}, POST_SERVE_POLL_MS);
}
private void scheduleReturnHome(final int kickedHabboId, final int roomId, final int waitedMs) {
Room currentRoom = this.getRoom();
if (currentRoom == null || currentRoom.getId() != roomId) return;
boolean stillEscorting = currentRoom.getHabbo(kickedHabboId) != null;
if (!stillEscorting || waitedMs >= RETURN_HOME_MAX_WAIT_MS) {
this.returnHome(kickedHabboId, true);
return;
}
Emulator.getThreading().run(
() -> this.scheduleReturnHome(kickedHabboId, roomId, waitedMs + RETURN_HOME_POLL_MS),
RETURN_HOME_POLL_MS);
}
private void returnHome(int kickedHabboId, boolean alwaysTeleport) {
final Room room = this.getRoom();
if (room == null || this.homeTile == null || this.getRoomUnit() == null) {
this.busy.set(false);
return;
}
final Runnable teleportHome = () -> {
Room r = this.getRoom();
if (r == null || this.getRoomUnit() == null) return;
double homeZ = r.getTopHeightAt(this.homeTile.x, this.homeTile.y);
this.getRoomUnit().stopWalking();
this.getRoomUnit().setZ(homeZ);
this.getRoomUnit().setLocation(this.homeTile);
this.getRoomUnit().setPreviousLocationZ(homeZ);
if (this.homeRotation != null) {
this.getRoomUnit().setRotation(this.homeRotation);
}
this.getRoomUnit().statusUpdate(true);
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
this.persistPosition();
};
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
if (this.homeRotation != null) {
this.getRoomUnit().setRotation(this.homeRotation);
room.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
}
this.persistPosition();
return;
}
boolean hasOtherWatchers = false;
for (Habbo h : room.getCurrentHabbos().values()) {
if (h.getHabboInfo().getId() != kickedHabboId) {
hasOtherWatchers = true;
break;
}
}
if (alwaysTeleport || !hasOtherWatchers || !this.getRoomUnit().canWalk()) {
teleportHome.run();
return;
}
List<Runnable> onArrive = new ArrayList<>();
onArrive.add(() -> {
if (this.homeRotation != null && this.getRoom() != null) {
this.getRoomUnit().setRotation(this.homeRotation);
this.getRoom().sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
}
this.persistPosition();
});
List<Runnable> onFail = new ArrayList<>();
onFail.add(teleportHome);
this.getRoomUnit().setGoalLocation(this.homeTile);
Emulator.getThreading().run(
new RoomUnitWalkToLocation(this.getRoomUnit(), this.homeTile, room, onArrive, onFail));
}
private void persistPosition() {
this.needsUpdate(true);
// Synchronous so the DB row reflects the home tile / rotation
// we just set, before busy clears and another onUserSay can
// race in and mutate position.
this.run();
this.busy.set(false);
}
}
@@ -1046,10 +1046,22 @@ public class CatalogManager {
for (Item baseItem : item.getBaseItems()) { for (Item baseItem : item.getBaseItems()) {
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) { for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) { if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
String baseName = baseItem.getName();
String type = item.getName().replace("rentable_bot_", ""); String type = item.getName().replace("rentable_bot_", "");
type = type.replace("bot_", ""); type = type.replace("bot_", "");
type = type.replace("visitor_logger", "visitor_log"); type = type.replace("visitor_logger", "visitor_log");
// Permission gate keyed on the canonical base-item name
// (admin-controlled but stable), not the catalog page name
// which can be renamed and bypass the check.
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
}
THashMap<String, String> data = new THashMap<>(); THashMap<String, String> data = new THashMap<>();
for (String s : item.getExtradata().split(";")) { for (String s : item.getExtradata().split(";")) {
@@ -175,6 +175,10 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogItem item = page.getCatalogItem(itemId); CatalogItem item = page.getCatalogItem(itemId);
// Search-results gift sends the catalog offer_id as
// itemId, not catalog_items.id - see the same fix in
// CatalogBuyItemEvent. Fall back to scanning the
// page for the matching offer_id.
if (item == null) { if (item == null) {
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) { for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
if (candidate != null && candidate.getOfferId() == itemId) { if (candidate != null && candidate.getOfferId() == itemId) {
@@ -14,7 +14,11 @@ import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.habbohotel.users.HabboInventory; import com.eu.habbo.habbohotel.users.HabboInventory;
import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.habbohotel.users.subscriptions.Subscription;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.*; import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseFailedComposer;
import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseUnavailableComposer;
import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer;
import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer;
import com.eu.habbo.messages.outgoing.catalog.PurchaseOKComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer; import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer;
@@ -199,6 +203,12 @@ public class CatalogBuyItemEvent extends MessageHandler {
else else
item = page.getCatalogItem(itemId); item = page.getCatalogItem(itemId);
// Search-results buy sends the catalog offer_id as itemId
// (FurnitureOffer.offerId is derived from furnidata's
// purchaseOfferId, which matches `catalog_items.offer_id`),
// not the `catalog_items.id` primary key that getCatalogItem
// expects. Fall back to scanning the page for the matching
// offer_id so the search → buy flow works.
if (item == null && !(page instanceof RecentPurchasesLayout)) { if (item == null && !(page instanceof RecentPurchasesLayout)) {
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) { for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
if (candidate != null && candidate.getOfferId() == itemId) { if (candidate != null && candidate.getOfferId() == itemId) {
@@ -207,7 +217,13 @@ public class CatalogBuyItemEvent extends MessageHandler {
} }
} }
} }
// Inventory cap check based on the actual base items the
// purchase will create, not the page layout - bots/pets
// can legitimately live on bundle pages, search results,
// recent-purchases, etc., and the layout-instanceof check
// missed all those paths. Mirrors the bot/pet branches
// inside CatalogManager.purchaseItem (Item.isBot / isPet
// and the same prefix check) so detection stays in sync.
boolean itemHasBot = false; boolean itemHasBot = false;
boolean itemHasPet = false; boolean itemHasPet = false;
@@ -17,7 +17,11 @@ import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.*; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FloorPlanEditorSaveEvent extends MessageHandler { public class FloorPlanEditorSaveEvent extends MessageHandler {