diff --git a/Database Updates/007_Frank.sql b/Database Updates/007_Frank.sql new file mode 100644 index 00000000..96dfd4ae --- /dev/null +++ b/Database Updates/007_Frank.sql @@ -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', 'r', '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'); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java index 5c5e5d14..9123c338 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java @@ -539,5 +539,9 @@ public class Bot implements Runnable { } } + private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + public short[] getOwnerActionIds() { + return DEFAULT_OWNER_ACTION_IDS; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java index 80f820d3..ff2b1d72 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java @@ -41,6 +41,7 @@ public class BotManager { addBotDefinition("generic", Bot.class); addBotDefinition("bartender", ButlerBot.class); addBotDefinition("visitor_log", VisitorBot.class); + addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class); this.reload(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java new file mode 100644 index 00000000..a373f0fc --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java @@ -0,0 +1,449 @@ +package com.eu.habbo.habbohotel.bots; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.*; +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; + +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"; + private static final List 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> chatResponses = new ConcurrentHashMap<>(); + private static volatile List 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 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(); + } + } + + private static final short[] FRANK_OWNER_ACTIONS = new short[0]; + + @Override + public short[] getOwnerActionIds() { + return FRANK_OWNER_ACTIONS; + } + + 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 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 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) { + 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 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> entry : chatResponses.entrySet()) { + if (entry.getKey().matcher(lower).find()) { + List 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 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 onArrive = new ArrayList<>(); + onArrive.add(kickThenReturn); + + List 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 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 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); + this.run(); + this.busy.set(false); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 8ac07faf..3ea4b00b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -1046,10 +1046,22 @@ public class CatalogManager { for (Item baseItem : item.getBaseItems()) { for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) { if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) { + String baseName = baseItem.getName(); String type = item.getName().replace("rentable_bot_", ""); type = type.replace("bot_", ""); 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 data = new THashMap<>(); for (String s : item.getExtradata().split(";")) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java index 2a208c2f..992ef724 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java @@ -175,6 +175,10 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { 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) { for (CatalogItem candidate : page.getCatalogItems().valueCollection()) { if (candidate != null && candidate.getOfferId() == itemId) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java index e948a5eb..a80067b7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java @@ -14,7 +14,11 @@ import com.eu.habbo.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.users.HabboInventory; import com.eu.habbo.habbohotel.users.subscriptions.Subscription; 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.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer; @@ -199,6 +203,12 @@ public class CatalogBuyItemEvent extends MessageHandler { else 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)) { for (CatalogItem candidate : page.getCatalogItems().valueCollection()) { 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 itemHasPet = false; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java index bd2bd76a..f9e7d3b4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java @@ -17,7 +17,11 @@ import gnu.trove.set.hash.THashSet; import org.slf4j.Logger; 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; public class FloorPlanEditorSaveEvent extends MessageHandler { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java index 0622a6b2..6d7c8d21 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java @@ -34,6 +34,9 @@ public class BotSaveSettingsEvent extends MessageHandler { if (bot == null) return; + if (bot.getOwnerActionIds().length == 0) + return; + int settingId = this.packet.readInt(); switch (settingId) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java index 8b651b48..51e00c4c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java @@ -86,7 +86,7 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendInt(habbo.getHabboInfo().getId()); this.response.appendString(habbo.getHabboInfo().getUsername()); this.response.appendString(habbo.getHabboInfo().getMotto()); - this.response.appendInt(habbo.getHabboInfo().getInfostandBg()); + this.response.appendInt(habbo.getHabboInfo().getInfostandBg()); this.response.appendInt(habbo.getHabboInfo().getInfostandStand()); this.response.appendInt(habbo.getHabboInfo().getInfostandOverlay()); this.response.appendInt(habbo.getHabboInfo().getInfostandCardBg()); @@ -129,7 +129,7 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendInt(0 - this.bot.getId()); this.response.appendString(this.bot.getName()); this.response.appendString(this.bot.getMotto()); - this.response.appendInt(0); + this.response.appendInt(0); this.response.appendInt(0); this.response.appendInt(0); this.response.appendInt(0); @@ -143,17 +143,11 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(this.bot.getGender().name().toUpperCase()); this.response.appendInt(this.bot.getOwnerId()); this.response.appendString(this.bot.getOwnerName()); - this.response.appendInt(10); - this.response.appendShort(0); - this.response.appendShort(1); - this.response.appendShort(2); - this.response.appendShort(3); - this.response.appendShort(4); - this.response.appendShort(5); - this.response.appendShort(6); - this.response.appendShort(7); - this.response.appendShort(8); - this.response.appendShort(9); + short[] singleActions = this.bot.getOwnerActionIds(); + this.response.appendInt(singleActions.length); + for (short action : singleActions) { + this.response.appendShort(action); + } this.response.appendString("unknown"); this.response.appendInt(0); this.response.appendInt(0); @@ -163,10 +157,10 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendInt(0 - bot.getId()); this.response.appendString(bot.getName()); this.response.appendString(bot.getMotto()); - this.response.appendInt(0); - this.response.appendInt(0); - this.response.appendInt(0); - this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); this.response.appendString(bot.getFigure()); this.response.appendInt(bot.getRoomUnit().getId()); this.response.appendInt(bot.getRoomUnit().getX()); @@ -177,17 +171,11 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(bot.getGender().name().toUpperCase()); this.response.appendInt(bot.getOwnerId()); this.response.appendString(bot.getOwnerName()); - this.response.appendInt(10); - this.response.appendShort(0); - this.response.appendShort(1); - this.response.appendShort(2); - this.response.appendShort(3); - this.response.appendShort(4); - this.response.appendShort(5); - this.response.appendShort(6); - this.response.appendShort(7); - this.response.appendShort(8); - this.response.appendShort(9); + short[] listActions = bot.getOwnerActionIds(); + this.response.appendInt(listActions.length); + for (short action : listActions) { + this.response.appendShort(action); + } this.response.appendString("unknown"); this.response.appendInt(0); this.response.appendInt(0);