diff --git a/Database Updates/014_Make_Custom_Badge.sql b/Database Updates/014_Make_Custom_Badge.sql new file mode 100644 index 00000000..3fc97157 --- /dev/null +++ b/Database Updates/014_Make_Custom_Badge.sql @@ -0,0 +1,28 @@ +-- Make sure that the emulator has write access to the badge_path folder !!!!! + +CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `badge_path` varchar(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584', + `badge_url` varchar(255) NOT NULL DEFAULT '/gamedata/c_images/album1584', + `price_badge` int(11) NOT NULL DEFAULT 0, + `currency_type` int(11) NOT NULL DEFAULT -1, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`) +SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5 +WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1); + +CREATE TABLE IF NOT EXISTS `user_custom_badge` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `badge_id` varchar(64) NOT NULL, + `badge_name` varchar(64) NOT NULL DEFAULT '', + `badge_description` varchar(255) NOT NULL DEFAULT '', + `date_created` int(11) NOT NULL DEFAULT 0, + `date_edit` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `badge_id` (`badge_id`), + KEY `user_id` (`user_id`), + CONSTRAINT `fk_user_custom_badge_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; \ No newline at end of file diff --git a/Database Updates/015_add_users_background_card_id.sql b/Database Updates/015_add_users_background_card_id.sql new file mode 100644 index 00000000..86b27393 --- /dev/null +++ b/Database Updates/015_add_users_background_card_id.sql @@ -0,0 +1 @@ +ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`; diff --git a/Emulator/sqlupdates/custom_prefixes_setup.sql b/Database Updates/016_custom_prefixes_setup.sql similarity index 68% rename from Emulator/sqlupdates/custom_prefixes_setup.sql rename to Database Updates/016_custom_prefixes_setup.sql index 7d5b22c5..dd2b857a 100644 --- a/Emulator/sqlupdates/custom_prefixes_setup.sql +++ b/Database Updates/016_custom_prefixes_setup.sql @@ -1,8 +1,3 @@ --- ============================================================ --- Custom Prefix System - Complete Setup --- ============================================================ - --- 1. Main user prefixes table CREATE TABLE IF NOT EXISTS `user_prefixes` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, @@ -46,34 +41,6 @@ INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES ('mod'), ('owner'); --- 4. Add effect column (if table already exists without it) --- ALTER TABLE `user_prefixes` ADD COLUMN IF NOT EXISTS `effect` VARCHAR(50) NOT NULL DEFAULT '' AFTER `icon`; - --- ============================================================ --- Catalog page for custom prefixes --- ============================================================ --- NOTE: Adjust parent_id to match your catalog parent category ID. --- Example: parent_id = -1 for root, or the ID of your "Extra" / "Specials" category - -INSERT INTO `catalog_pages` ( - `parent_id`, `caption`, `caption_save`, `icon_image`, `visible`, `enabled`, - `min_rank`, `page_layout`, `page_strings_1`, `page_strings_2` -) VALUES ( - -1, - 'Custom Prefix', - 'custom_prefix', - 1, - 1, - 1, - 1, - 'custom_prefix', - 'Create your own custom prefix!\rChoose text, colors, icon and effects to stand out in chat.', - '' -); - --- ============================================================ --- Command texts (insert into emulator_texts if not present) --- ============================================================ INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES -- GivePrefix command ('commands.keys.cmd_give_prefix', 'giveprefix'), @@ -105,11 +72,11 @@ INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES ('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'), ('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.'); --- ============================================================ --- Permissions for prefix commands (add to permissions table) --- ============================================================ -INSERT IGNORE INTO `permissions` (`id`, `rank_id`, `permission_name`, `setting_type`) VALUES - (NULL, 7, 'cmd_give_prefix', '1'), - (NULL, 7, 'cmd_list_prefixes', '1'), - (NULL, 7, 'cmd_remove_prefix', '1'), - (NULL, 7, 'cmd_prefix_blacklist', '1'); +INSERT IGNORE INTO permission_definitions +(permission_key, max_value, rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7) +VALUES +('cmd_give_prefix', '1', '0', '0', '0', '0', '0', '0', '1'), +('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'), +('cmd_remove_prefix', '1', '0', '0', '0', '0', '0', '0', '1'), +('cmd_prefix_blacklist', '1', '0', '0', '0', '0', '0', '0', '1'); + diff --git a/Emulator/sqlupdates/catalog_admin_permission.sql b/Emulator/sqlupdates/catalog_admin_permission.sql deleted file mode 100644 index 65eb459f..00000000 --- a/Emulator/sqlupdates/catalog_admin_permission.sql +++ /dev/null @@ -1,17 +0,0 @@ --- ============================================================ --- Catalog & Furni Admin Permission --- Adds acc_catalogfurni permission to the permissions table --- Required by: CatalogAdmin packet handlers (10050-10059) --- ============================================================ - --- 1. Add the column to the permissions table -ALTER TABLE `permissions` - ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0' - AFTER `acc_catalog_ids`; - --- 2. Enable for Administrator (rank 7) by default -UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 7; - --- Optional: enable for other ranks as needed --- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 6; -- Super Mod --- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 5; -- Moderator diff --git a/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java b/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java index 73d7992a..825f7dcf 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java @@ -28,6 +28,7 @@ public class RoomUserPetComposer extends MessageComposer { this.response.appendInt(0); this.response.appendInt(0); this.response.appendInt(0); + this.response.appendInt(0); this.response.appendString(this.petType + " " + this.race + " " + this.color + " 2 2 -1 0 3 -1 0"); this.response.appendInt(this.habbo.getRoomUnit().getId()); this.response.appendInt(this.habbo.getRoomUnit().getX()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java index 879e2d6b..ac401c1f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java @@ -22,6 +22,7 @@ import com.eu.habbo.habbohotel.polls.PollManager; import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager; import com.eu.habbo.habbohotel.rooms.RoomManager; import com.eu.habbo.habbohotel.users.HabboManager; +import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionManager; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionScheduler; import org.slf4j.Logger; @@ -58,6 +59,7 @@ public class GameEnvironment { private SubscriptionManager subscriptionManager; private CalendarManager calendarManager; private RoomChatBubbleManager roomChatBubbleManager; + private CustomBadgeManager customBadgeManager; public void load() throws Exception { LOGGER.info("GameEnvironment -> Loading..."); @@ -84,6 +86,7 @@ public class GameEnvironment { this.pollManager = new PollManager(); this.calendarManager = new CalendarManager(); this.roomChatBubbleManager = new RoomChatBubbleManager(); + this.customBadgeManager = new CustomBadgeManager(); this.roomManager.loadPublicRooms(); this.navigatorManager.loadNavigator(); @@ -219,4 +222,8 @@ public class GameEnvironment { public RoomChatBubbleManager getRoomChatBubbleManager() { return roomChatBubbleManager; } + + public CustomBadgeManager getCustomBadgeManager() { + return this.customBadgeManager; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java index f2724578..1f654d35 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java @@ -1,23 +1,16 @@ package com.eu.habbo.habbohotel.gameclients; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserEffectComposer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; -/** - * Manages a grace period for disconnected users. Instead of immediately - * disposing a Habbo when their WebSocket drops, the Habbo is held in - * a "ghost" state for a configurable number of seconds. If the same - * user reconnects (via SSO ticket) within the grace window, their - * existing Habbo object is resumed on the new connection — keeping - * them in their room, preserving inventory state, etc. - * - * Config key: session.reconnect.grace.seconds (default: 30) - */ public class SessionResumeManager { private static final Logger LOGGER = LoggerFactory.getLogger(SessionResumeManager.class); @@ -37,12 +30,10 @@ public class SessionResumeManager { return Emulator.getConfig().getInt("session.reconnect.grace.seconds", 30); } - /** - * Park a disconnected Habbo in ghost mode. Their room presence is - * preserved, but the old GameClient channel is closed. - * - * @return true if the habbo was parked (grace period > 0), false if immediate dispose should happen - */ + public int getPausedEffectId() { + return Emulator.getConfig().getInt("session.reconnect.effect.id", 170); + } + public boolean parkHabbo(Habbo habbo, String ssoTicket) { int graceSeconds = getGracePeriodSeconds(); if (graceSeconds <= 0) { @@ -51,7 +42,6 @@ public class SessionResumeManager { int userId = habbo.getHabboInfo().getId(); - // Cancel any existing ghost session for this user GhostSession existing = ghostSessions.remove(userId); if (existing != null && existing.disposeFuture != null) { existing.disposeFuture.cancel(false); @@ -60,12 +50,18 @@ public class SessionResumeManager { LOGGER.info("[SessionResume] Parking {} (id={}) for {}s grace period", habbo.getHabboInfo().getUsername(), userId, graceSeconds); - // Restore the SSO ticket so the client can reconnect with the same ticket if (ssoTicket != null && !ssoTicket.isEmpty()) { restoreSsoTicket(userId, ssoTicket); } - // Schedule the final disconnect after the grace period + int previousEffectId = 0; + int previousEffectEnd = 0; + RoomUnit unit = habbo.getRoomUnit(); + if (unit != null) { + previousEffectId = unit.getEffectId(); + previousEffectEnd = unit.getEffectEndTimestamp(); + } + ScheduledFuture future = Emulator.getThreading().run(() -> { GhostSession ghost = ghostSessions.remove(userId); if (ghost != null) { @@ -75,22 +71,19 @@ public class SessionResumeManager { } }, graceSeconds * 1000); - ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future)); + ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd)); + + applyPausedEffect(habbo); + return true; } - /** - * Try to resume a ghost session for the given user ID. - * - * @return the parked Habbo if found within grace period, null otherwise - */ public Habbo resumeSession(int userId) { GhostSession ghost = ghostSessions.remove(userId); if (ghost == null) { return null; } - // Cancel the scheduled dispose if (ghost.disposeFuture != null) { ghost.disposeFuture.cancel(false); } @@ -98,19 +91,15 @@ public class SessionResumeManager { LOGGER.info("[SessionResume] Resuming session for {} (id={})", ghost.habbo.getHabboInfo().getUsername(), userId); + restorePausedEffect(ghost); + return ghost.habbo; } - /** - * Check if a user has a ghost session (is in grace period). - */ public boolean hasGhostSession(int userId) { return ghostSessions.containsKey(userId); } - /** - * Immediately expire all ghost sessions (e.g. on emulator shutdown). - */ public void disposeAll() { for (GhostSession ghost : ghostSessions.values()) { if (ghost.disposeFuture != null) { @@ -121,9 +110,6 @@ public class SessionResumeManager { ghostSessions.clear(); } - /** - * Perform the actual full disconnect that normally happens in Habbo.disconnect(). - */ private void performFullDisconnect(Habbo habbo) { try { habbo.getHabboInfo().setOnline(false); @@ -132,7 +118,6 @@ public class SessionResumeManager { LOGGER.error("[SessionResume] Error during deferred disconnect", e); } - // Clear the SSO ticket now that the grace period is truly over clearSsoTicket(habbo.getHabboInfo().getId()); } @@ -148,6 +133,38 @@ public class SessionResumeManager { } } + private void applyPausedEffect(Habbo habbo) { + int effectId = getPausedEffectId(); + if (effectId <= 0) return; + try { + RoomUnit unit = habbo.getRoomUnit(); + Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom(); + if (unit == null || room == null) return; + int endTimestamp = Emulator.getIntUnixTimestamp() + getGracePeriodSeconds() + 10; + unit.setEffectId(effectId, endTimestamp); + room.sendComposer(new RoomUserEffectComposer(unit).compose()); + } catch (Exception e) { + LOGGER.error("[SessionResume] Failed to apply paused effect", e); + } + } + + private void restorePausedEffect(GhostSession ghost) { + try { + Habbo habbo = ghost.habbo; + RoomUnit unit = habbo.getRoomUnit(); + Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom(); + if (unit == null || room == null) return; + + int pausedEffectId = getPausedEffectId(); + if (unit.getEffectId() == pausedEffectId) { + unit.setEffectId(ghost.previousEffectId, ghost.previousEffectEnd); + room.sendComposer(new RoomUserEffectComposer(unit).compose()); + } + } catch (Exception e) { + LOGGER.error("[SessionResume] Failed to restore previous effect", e); + } + } + private void clearSsoTicket(int userId) { try (var connection = Emulator.getDatabase().getDataSource().getConnection(); var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) { @@ -163,11 +180,16 @@ public class SessionResumeManager { final Habbo habbo; final String ssoTicket; final ScheduledFuture disposeFuture; + final int previousEffectId; + final int previousEffectEnd; - GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture disposeFuture) { + GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture disposeFuture, + int previousEffectId, int previousEffectEnd) { this.habbo = habbo; this.ssoTicket = ssoTicket; this.disposeFuture = disposeFuture; + this.previousEffectId = previousEffectId; + this.previousEffectEnd = previousEffectEnd; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index a40bdbf6..4769dd1d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -15,25 +15,19 @@ import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagField; import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagPole; import com.eu.habbo.habbohotel.items.interactions.pets.*; import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector; -import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable; -import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.*; import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboInfo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.users.HabboManager; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics; import com.eu.habbo.habbohotel.wired.tick.WiredTickable; import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer; +import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer; import com.eu.habbo.messages.outgoing.rooms.items.*; import com.eu.habbo.plugin.Event; import com.eu.habbo.plugin.events.furniture.*; @@ -94,7 +88,7 @@ public class RoomItemManager { } try (PreparedStatement statement = connection.prepareStatement( - "SELECT * FROM items WHERE room_id = ?")) { + "SELECT * FROM items WHERE room_id = ?")) { statement.setInt(1, this.room.getId()); try (ResultSet set = statement.executeQuery()) { while (set.next()) { @@ -106,8 +100,8 @@ public class RoomItemManager { } if (this.itemCount() > Room.MAXIMUM_FURNI) { - LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).", - this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI); + LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).", + this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI); } } @@ -116,7 +110,7 @@ public class RoomItemManager { */ public void loadWiredData(Connection connection) { try (PreparedStatement statement = connection.prepareStatement( - "SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) { + "SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) { statement.setInt(1, this.room.getId()); try (ResultSet set = statement.executeQuery()) { @@ -274,7 +268,7 @@ public class RoomItemManager { } if (iterator.value().getBaseItem().getInteractionType().getType() - == InteractionPostIt.class) { + == InteractionPostIt.class) { items.add(iterator.value()); } } @@ -359,7 +353,7 @@ public class RoomItemManager { } if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY() - && tile.y <= item.getY() + length - 1)) { + && tile.y <= item.getY() + length - 1)) { continue; } @@ -447,7 +441,7 @@ public class RoomItemManager { } if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem) - > item.getZ() + Item.getCurrentHeight(item)) { + > item.getZ() + Item.getCurrentHeight(item)) { continue; } @@ -516,7 +510,7 @@ public class RoomItemManager { } if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem) - > item.getZ() + Item.getCurrentHeight(item)) { + > item.getZ() + Item.getCurrentHeight(item)) { continue; } @@ -598,7 +592,7 @@ public class RoomItemManager { } if (lowestChair != null && lowestChair.getZ() + Item.getCurrentHeight(lowestChair) - > item.getZ() + Item.getCurrentHeight(item)) { + > item.getZ() + Item.getCurrentHeight(item)) { continue; } @@ -647,7 +641,7 @@ public class RoomItemManager { this.furniOwnerNames.put(item.getUserId(), habbo.getUsername()); } else { LOGGER.error("Failed to find username for item (ID: {}, UserID: {})", - item.getId(), item.getUserId()); + item.getId(), item.getUserId()); } } } @@ -665,7 +659,7 @@ public class RoomItemManager { if (specialTypes == null) { return; } - + boolean isWiredItem = false; synchronized (specialTypes) { @@ -714,29 +708,29 @@ public class RoomItemManager { } else if (item instanceof InteractionPetTree) { specialTypes.addPetTree((InteractionPetTree) item); } else if (item instanceof InteractionMoodLight || - item instanceof InteractionPyramid || - item instanceof InteractionMusicDisc || - item instanceof InteractionBattleBanzaiSphere || - item instanceof InteractionTalkingFurniture || - item instanceof InteractionWater || - item instanceof InteractionWaterItem || - item instanceof InteractionMuteArea || - item instanceof InteractionBuildArea || - item instanceof InteractionTagPole || - item instanceof InteractionTagField || - item instanceof InteractionJukeBox || - item instanceof InteractionPetBreedingNest || - item instanceof InteractionBlackHole || - item instanceof InteractionWiredHighscore || - item instanceof InteractionStickyPole || - item instanceof WiredBlob || - item instanceof InteractionTent || - item instanceof InteractionSnowboardSlope || - item instanceof InteractionFireworks) { + item instanceof InteractionPyramid || + item instanceof InteractionMusicDisc || + item instanceof InteractionBattleBanzaiSphere || + item instanceof InteractionTalkingFurniture || + item instanceof InteractionWater || + item instanceof InteractionWaterItem || + item instanceof InteractionMuteArea || + item instanceof InteractionBuildArea || + item instanceof InteractionTagPole || + item instanceof InteractionTagField || + item instanceof InteractionJukeBox || + item instanceof InteractionPetBreedingNest || + item instanceof InteractionBlackHole || + item instanceof InteractionWiredHighscore || + item instanceof InteractionStickyPole || + item instanceof WiredBlob || + item instanceof InteractionTent || + item instanceof InteractionSnowboardSlope || + item instanceof InteractionFireworks) { specialTypes.addUndefined(item); } } - + // Invalidate wired cache when wired items are added if (isWiredItem) { WiredManager.invalidateRoom(this.room); @@ -810,7 +804,7 @@ public class RoomItemManager { } this.room.getFurniVariableManager().removeAssignmentsForFurni(item.getId()); - + boolean isWiredItem = false; // Unregister from tick service for time-based wired triggers (new 50ms tick system) @@ -822,53 +816,53 @@ public class RoomItemManager { specialTypes.removeCycleTask((ICycleable) item); } - if (item instanceof InteractionBattleBanzaiTeleporter) { - specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item); - } else if (item instanceof InteractionWiredTrigger) { - specialTypes.removeTrigger((InteractionWiredTrigger) item); - isWiredItem = true; - } else if (item instanceof InteractionWiredEffect) { - specialTypes.removeEffect((InteractionWiredEffect) item); - isWiredItem = true; - } else if (item instanceof InteractionWiredCondition) { - specialTypes.removeCondition((InteractionWiredCondition) item); - isWiredItem = true; - } else if (item instanceof InteractionWiredExtra) { - boolean removedContextDefinition = false; - boolean removedVariableTextConnector = false; - if (item instanceof WiredExtraUserVariable) { - this.room.getUserVariableManager().removeDefinition(item.getId()); - } else if (item instanceof WiredExtraFurniVariable) { - this.room.getFurniVariableManager().removeDefinition(item.getId()); - } else if (item instanceof WiredExtraRoomVariable) { - this.room.getRoomVariableManager().removeDefinition(item.getId()); - } else if (item instanceof WiredExtraContextVariable) { - removedContextDefinition = true; - } else if (item instanceof WiredExtraVariableTextConnector) { - removedVariableTextConnector = true; - } else if (item instanceof WiredExtraVariableReference) { - if (((WiredExtraVariableReference) item).isRoomReference()) { - this.room.getRoomVariableManager().removeDefinition(item.getId()); - } else { - this.room.getUserVariableManager().removeDefinition(item.getId()); - } - } else if (item instanceof WiredExtraVariableEcho) { - WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item; + if (item instanceof InteractionBattleBanzaiTeleporter) { + specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item); + } else if (item instanceof InteractionWiredTrigger) { + specialTypes.removeTrigger((InteractionWiredTrigger) item); + isWiredItem = true; + } else if (item instanceof InteractionWiredEffect) { + specialTypes.removeEffect((InteractionWiredEffect) item); + isWiredItem = true; + } else if (item instanceof InteractionWiredCondition) { + specialTypes.removeCondition((InteractionWiredCondition) item); + isWiredItem = true; + } else if (item instanceof InteractionWiredExtra) { + boolean removedContextDefinition = false; + boolean removedVariableTextConnector = false; + if (item instanceof WiredExtraUserVariable) { + this.room.getUserVariableManager().removeDefinition(item.getId()); + } else if (item instanceof WiredExtraFurniVariable) { + this.room.getFurniVariableManager().removeDefinition(item.getId()); + } else if (item instanceof WiredExtraRoomVariable) { + this.room.getRoomVariableManager().removeDefinition(item.getId()); + } else if (item instanceof WiredExtraContextVariable) { + removedContextDefinition = true; + } else if (item instanceof WiredExtraVariableTextConnector) { + removedVariableTextConnector = true; + } else if (item instanceof WiredExtraVariableReference) { + if (((WiredExtraVariableReference) item).isRoomReference()) { + this.room.getRoomVariableManager().removeDefinition(item.getId()); + } else { + this.room.getUserVariableManager().removeDefinition(item.getId()); + } + } else if (item instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item; - if (echo.isRoomEcho()) { - this.room.getRoomVariableManager().removeDefinition(item.getId()); - } else if (echo.isFurniEcho()) { - this.room.getFurniVariableManager().removeDefinition(item.getId()); - } else { - this.room.getUserVariableManager().removeDefinition(item.getId()); - } - } - specialTypes.removeExtra((InteractionWiredExtra) item); - if (removedContextDefinition || removedVariableTextConnector) { - WiredContextVariableSupport.broadcastDefinitions(this.room); - } - isWiredItem = true; - } else if (item instanceof InteractionRoller) { + if (echo.isRoomEcho()) { + this.room.getRoomVariableManager().removeDefinition(item.getId()); + } else if (echo.isFurniEcho()) { + this.room.getFurniVariableManager().removeDefinition(item.getId()); + } else { + this.room.getUserVariableManager().removeDefinition(item.getId()); + } + } + specialTypes.removeExtra((InteractionWiredExtra) item); + if (removedContextDefinition || removedVariableTextConnector) { + WiredContextVariableSupport.broadcastDefinitions(this.room); + } + isWiredItem = true; + } else if (item instanceof InteractionRoller) { specialTypes.removeRoller((InteractionRoller) item); } else if (item instanceof InteractionGameScoreboard) { specialTypes.removeScoreboard((InteractionGameScoreboard) item); @@ -889,26 +883,26 @@ public class RoomItemManager { } else if (item instanceof InteractionPetTree) { specialTypes.removePetTree((InteractionPetTree) item); } else if (item instanceof InteractionMoodLight || - item instanceof InteractionPyramid || - item instanceof InteractionMusicDisc || - item instanceof InteractionBattleBanzaiSphere || - item instanceof InteractionTalkingFurniture || - item instanceof InteractionWaterItem || - item instanceof InteractionWater || - item instanceof InteractionMuteArea || - item instanceof InteractionTagPole || - item instanceof InteractionTagField || - item instanceof InteractionJukeBox || - item instanceof InteractionPetBreedingNest || - item instanceof InteractionBlackHole || - item instanceof InteractionWiredHighscore || - item instanceof InteractionStickyPole || - item instanceof WiredBlob || - item instanceof InteractionTent || - item instanceof InteractionSnowboardSlope) { + item instanceof InteractionPyramid || + item instanceof InteractionMusicDisc || + item instanceof InteractionBattleBanzaiSphere || + item instanceof InteractionTalkingFurniture || + item instanceof InteractionWaterItem || + item instanceof InteractionWater || + item instanceof InteractionMuteArea || + item instanceof InteractionTagPole || + item instanceof InteractionTagField || + item instanceof InteractionJukeBox || + item instanceof InteractionPetBreedingNest || + item instanceof InteractionBlackHole || + item instanceof InteractionWiredHighscore || + item instanceof InteractionStickyPole || + item instanceof WiredBlob || + item instanceof InteractionTent || + item instanceof InteractionSnowboardSlope) { specialTypes.removeUndefined(item); } - + // Invalidate wired cache when wired items are removed if (isWiredItem || cleanedSignalAntennaReferences) { WiredManager.invalidateRoom(this.room); @@ -936,9 +930,9 @@ public class RoomItemManager { if (item.getBaseItem().getType() == FurnitureType.FLOOR) { this.room.sendComposer(new FloorItemUpdateComposer(item).compose()); this.room.updateTiles(this.room.getLayout() - .getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()), - item.getBaseItem().getWidth(), item.getBaseItem().getLength(), - item.getRotation())); + .getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()), + item.getBaseItem().getWidth(), item.getBaseItem().getLength(), + item.getRotation())); } else if (item.getBaseItem().getType() == FurnitureType.WALL) { this.room.sendComposer(new WallItemUpdateComposer(item).compose()); } @@ -963,9 +957,9 @@ public class RoomItemManager { } this.room.updateTiles(this.room.getLayout() - .getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()), - item.getBaseItem().getWidth(), item.getBaseItem().getLength(), - item.getRotation())); + .getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()), + item.getBaseItem().getWidth(), item.getBaseItem().getLength(), + item.getRotation())); if (item instanceof InteractionMultiHeight) { ((InteractionMultiHeight) item).updateUnitsOnItem(this.room); @@ -1032,7 +1026,7 @@ public class RoomItemManager { if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) { FurniturePickedUpEvent event = Emulator.getPluginManager() - .fireEvent(new FurniturePickedUpEvent(item, picker)); + .fireEvent(new FurniturePickedUpEvent(item, picker)); if (event.isCancelled()) { return; @@ -1060,10 +1054,10 @@ public class RoomItemManager { } THashSet updatedTiles = this.room.getLayout().getTilesAt( - this.room.getLayout().getTile(item.getX(), item.getY()), - item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), - item.getRotation()); + this.room.getLayout().getTile(item.getX(), item.getY()), + item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), + item.getRotation()); this.room.updateTiles(updatedTiles); for (RoomTile tile : updatedTiles) { @@ -1114,6 +1108,7 @@ public class RoomItemManager { if (habbo != null && !inventoryItems.isEmpty()) { habbo.getInventory().getItemsComponent().addItems(inventoryItems); habbo.getClient().sendResponse(new AddHabboItemComposer(inventoryItems)); + habbo.getClient().sendResponse(new InventoryRefreshComposer()); } for (HabboItem i : items) { @@ -1160,7 +1155,7 @@ public class RoomItemManager { } userItemsMap.computeIfAbsent(iterator.value().getUserId(), k -> new THashSet<>()) - .add(iterator.value()); + .add(iterator.value()); } } @@ -1182,6 +1177,7 @@ public class RoomItemManager { if (user != null && !inventoryItems.isEmpty()) { user.getInventory().getItemsComponent().addItems(inventoryItems); user.getClient().sendResponse(new AddHabboItemComposer(inventoryItems)); + user.getClient().sendResponse(new InventoryRefreshComposer()); } } } @@ -1222,7 +1218,7 @@ public class RoomItemManager { for (short y = 0; y < item.getBaseItem().getLength(); y++) { for (short x = 0; x < item.getBaseItem().getWidth(); x++) { RoomTile tile = this.room.getLayout().getTile( - (short) (item.getX() + x), (short) (item.getY() + y)); + (short) (item.getX() + x), (short) (item.getY() + y)); if (tile != null) { lockedTiles.add(tile); @@ -1233,7 +1229,7 @@ public class RoomItemManager { for (short y = 0; y < item.getBaseItem().getWidth(); y++) { for (short x = 0; x < item.getBaseItem().getLength(); x++) { RoomTile tile = this.room.getLayout().getTile( - (short) (item.getX() + x), (short) (item.getY() + y)); + (short) (item.getX() + x), (short) (item.getY() + y)); if (tile != null) { lockedTiles.add(tile); @@ -1324,8 +1320,8 @@ public class RoomItemManager { rotation %= 8; if (this.room.hasRights(habbo) || this.room.getGuildRightLevel(habbo) - .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission( - Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) { + .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission( + Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) { return FurnitureMovementError.NONE; } @@ -1334,10 +1330,10 @@ public class RoomItemManager { if (rentSpace != null) { if (!RoomLayout.squareInSquare(RoomLayout.getRectangle(rentSpace.getX(), rentSpace.getY(), - rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(), - rentSpace.getRotation()), - RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation))) { + rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(), + rentSpace.getRotation()), + RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), rotation))) { return FurnitureMovementError.NO_RIGHTS; } else { return FurnitureMovementError.NONE; @@ -1347,7 +1343,7 @@ public class RoomItemManager { for (HabboItem area : this.room.getRoomSpecialTypes().getItemsOfType(InteractionBuildArea.class)) { if (((InteractionBuildArea) area).inSquare(tile) && ((InteractionBuildArea) area).isBuilder( - habbo.getHabboInfo().getUsername())) { + habbo.getHabboInfo().getUsername())) { return FurnitureMovementError.NONE; } } @@ -1438,14 +1434,14 @@ public class RoomItemManager { } THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); for (RoomTile t : occupiedTiles) { if (t.state == RoomTileState.INVALID) { return FurnitureMovementError.INVALID_MOVE; } if (!Emulator.getConfig().getBoolean("wired.place.under", false) || ( - Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable() - && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) { + Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable() + && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) { if (checkForUnits && this.room.hasHabbosAt(t.x, t.y)) { return FurnitureMovementError.TILE_HAS_HABBOS; } @@ -1490,7 +1486,7 @@ public class RoomItemManager { } THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); for (RoomTile t : occupiedTiles) { if (t.state == RoomTileState.INVALID) { return FurnitureMovementError.INVALID_MOVE; @@ -1542,7 +1538,7 @@ public class RoomItemManager { boolean pluginHelper = false; if (Emulator.getPluginManager().isRegistered(FurniturePlacedEvent.class, true)) { FurniturePlacedEvent event = Emulator.getPluginManager() - .fireEvent(new FurniturePlacedEvent(item, owner, tile)); + .fireEvent(new FurniturePlacedEvent(item, owner, tile)); if (event.isCancelled()) { return FurnitureMovementError.CANCEL_PLUGIN_PLACE; @@ -1553,7 +1549,7 @@ public class RoomItemManager { RoomLayout layout = this.room.getLayout(); THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); FurnitureMovementError fits = furnitureFitsAt(tile, item, rotation); @@ -1572,7 +1568,7 @@ public class RoomItemManager { if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) { FurnitureBuildheightEvent event = Emulator.getPluginManager() - .fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height)); + .fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height)); if (event.hasChangedHeight()) { height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight(); } @@ -1592,7 +1588,7 @@ public class RoomItemManager { item.onPlace(this.room); this.room.updateTiles(occupiedTiles); this.room.sendComposer( - new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); + new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) { RoomConfInvisSupport.sendState(this.room); @@ -1620,7 +1616,7 @@ public class RoomItemManager { */ public FurnitureMovementError placeWallFurniAt(HabboItem item, String wallPosition, Habbo owner) { if (!(this.room.hasRights(owner) || this.room.getGuildRightLevel(owner) - .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) { + .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) { return FurnitureMovementError.NO_RIGHTS; } @@ -1638,7 +1634,7 @@ public class RoomItemManager { this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner)); } this.room.sendComposer( - new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); + new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); item.needsUpdate(true); this.addHabboItem(item); item.setRoomId(this.room.getId()); @@ -1989,7 +1985,7 @@ public class RoomItemManager { boolean pluginHelper = false; if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) { FurnitureMovedEvent event = Emulator.getPluginManager() - .fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile)); + .fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile)); if (event.isCancelled()) { return FurnitureMovementError.CANCEL_PLUGIN_MOVE; } @@ -2002,9 +1998,9 @@ public class RoomItemManager { // Check if can be placed at new position THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); THashSet newOccupiedTiles = layout.getTilesAt(tile, - item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation); + item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation); HabboItem topItem = this.getTopItemAt(occupiedTiles, null); @@ -2013,15 +2009,15 @@ public class RoomItemManager { for (RoomTile t : occupiedTiles) { HabboItem tileTopItem = this.getTopItemAt(t.x, t.y); if (!magicTile && ((tileTopItem != null && tileTopItem != item ? ( - t.state.equals(RoomTileState.INVALID) || !t.getAllowStack() + t.state.equals(RoomTileState.INVALID) || !t.getAllowStack() || !tileTopItem.getBaseItem().allowStack()) - : this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) { + : this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) { return FurnitureMovementError.CANT_STACK; } if (!Emulator.getConfig().getBoolean("wired.place.under", false) || ( - Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable() - && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) { + Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable() + && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) { if (checkForUnits) { if (!magicTile && this.room.hasHabbosAt(t.x, t.y)) { return FurnitureMovementError.TILE_HAS_HABBOS; @@ -2048,8 +2044,8 @@ public class RoomItemManager { } THashSet oldOccupiedTiles = layout.getTilesAt( - layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), item.getRotation()); + layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), item.getRotation()); int oldRotation = item.getRotation(); @@ -2066,9 +2062,9 @@ public class RoomItemManager { } if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem() - .allowStack()) || (topItem != null && topItem != item - && topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item) - > Room.MAXIMUM_FURNI_HEIGHT)) { + .allowStack()) || (topItem != null && topItem != item + && topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item) + > Room.MAXIMUM_FURNI_HEIGHT)) { item.setRotation(oldRotation); return FurnitureMovementError.CANT_STACK; } @@ -2117,7 +2113,7 @@ public class RoomItemManager { if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) { FurnitureBuildheightEvent event = Emulator.getPluginManager() - .fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height)); + .fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height)); if (event.hasChangedHeight()) { height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight(); pluginHeight = true; @@ -2138,7 +2134,7 @@ public class RoomItemManager { if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { item.setZ(Room.MAXIMUM_FURNI_HEIGHT); } - + // Update wired spatial index and invalidate cache when wired items are moved if (item instanceof InteractionWiredTrigger) { this.room.getRoomSpecialTypes().updateTriggerLocation((InteractionWiredTrigger) item, oldLocation.x, oldLocation.y); @@ -2198,7 +2194,7 @@ public class RoomItemManager { boolean pluginHelper = false; if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) { FurnitureMovedEvent event = Emulator.getPluginManager() - .fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile)); + .fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile)); if (event.isCancelled()) { return FurnitureMovementError.CANCEL_PLUGIN_MOVE; } @@ -2210,9 +2206,9 @@ public class RoomItemManager { HabboItem stackHelper = this.findStackHeightHelperAt(tile, item); THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); THashSet newOccupiedTiles = layout.getTilesAt(tile, - item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation); + item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation); HabboItem topItem = this.getTopPhysicsItemAt(occupiedTiles, null, physics); @@ -2221,9 +2217,9 @@ public class RoomItemManager { for (RoomTile t : occupiedTiles) { HabboItem tileTopItem = this.getTopPhysicsItemAt(t.x, t.y, item, physics); if (!magicTile && ((tileTopItem != null && tileTopItem != item ? ( - t.state.equals(RoomTileState.INVALID) || !t.getAllowStack() + t.state.equals(RoomTileState.INVALID) || !t.getAllowStack() || !tileTopItem.getBaseItem().allowStack()) - : this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) { + : this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) { return FurnitureMovementError.CANT_STACK; } @@ -2251,8 +2247,8 @@ public class RoomItemManager { } THashSet oldOccupiedTiles = layout.getTilesAt( - layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), item.getRotation()); + layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), item.getRotation()); int oldRotation = item.getRotation(); @@ -2269,9 +2265,9 @@ public class RoomItemManager { } if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem() - .allowStack()) || (topItem != null && topItem != item - && topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item) - > Room.MAXIMUM_FURNI_HEIGHT)) { + .allowStack()) || (topItem != null && topItem != item + && topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item) + > Room.MAXIMUM_FURNI_HEIGHT)) { item.setRotation(oldRotation); return FurnitureMovementError.CANT_STACK; } @@ -2319,7 +2315,7 @@ public class RoomItemManager { if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) { FurnitureBuildheightEvent event = Emulator.getPluginManager() - .fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height)); + .fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height)); if (event.hasChangedHeight()) { height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight(); pluginHeight = true; @@ -2391,10 +2387,10 @@ public class RoomItemManager { boolean magicTile = this.isStackPlacementBypassItem(item); RoomLayout layout = this.room.getLayout(); - + // Check if can be placed at new position THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); java.util.List>> tileFurniList = new java.util.ArrayList<>(); for (RoomTile t : occupiedTiles) { @@ -2438,8 +2434,8 @@ public class RoomItemManager { } return !item.isWalkable() - && !item.getBaseItem().allowSit() - && !item.getBaseItem().allowLay(); + && !item.getBaseItem().allowSit() + && !item.getBaseItem().allowLay(); } private FurnitureMovementError getPhysicsUnitCollision(RoomTile tile, WiredMovementPhysics physics) { @@ -2515,7 +2511,7 @@ public class RoomItemManager { for (HabboItem item : this.getPhysicsItemsAt(tile, exclude, physics)) { if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem) - > item.getZ() + Item.getCurrentHeight(item)) { + > item.getZ() + Item.getCurrentHeight(item)) { continue; } @@ -2539,7 +2535,7 @@ public class RoomItemManager { } if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem) - > topItem.getZ() + Item.getCurrentHeight(topItem)) { + > topItem.getZ() + Item.getCurrentHeight(topItem)) { continue; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java index e53aee11..0bcc8247 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java @@ -45,6 +45,7 @@ public class HabboInfo implements Runnable { private int InfostandBg; private int InfostandStand; private int InfostandOverlay; + private int InfostandCardBg; private int loadingRoom; private Room currentRoom; private String roomEntryMethod = "door"; @@ -91,6 +92,7 @@ public class HabboInfo implements Runnable { this.InfostandBg = set.getInt("background_id"); this.InfostandStand = set.getInt("background_stand_id"); this.InfostandOverlay = set.getInt("background_overlay_id"); + this.InfostandCardBg = set.getInt("background_card_id"); this.currentRoom = null; } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); @@ -290,6 +292,14 @@ public class HabboInfo implements Runnable { public void setInfostandOverlay(int infostandOverlay) { InfostandOverlay = infostandOverlay; } + + public int getInfostandCardBg() { + return InfostandCardBg; + } + + public void setInfostandCardBg(int infostandCardBg) { + InfostandCardBg = infostandCardBg; + } public Rank getRank() { return this.rank; } @@ -577,7 +587,7 @@ public class HabboInfo implements Runnable { try { SqlQueries.update( - "UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ? WHERE id = ?", + "UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ? WHERE id = ?", this.motto, this.online ? "1" : "0", this.look, @@ -593,6 +603,7 @@ public class HabboInfo implements Runnable { this.InfostandBg, this.InfostandStand, this.InfostandOverlay, + this.InfostandCardBg, this.id); } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadge.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadge.java new file mode 100644 index 00000000..c80c9be7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadge.java @@ -0,0 +1,75 @@ +package com.eu.habbo.habbohotel.users.custombadge; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class CustomBadge { + + private final int id; + private final int userId; + private final String badgeId; + private String badgeName; + private String badgeDescription; + private final int dateCreated; + private int dateEdit; + + public CustomBadge(ResultSet set) throws SQLException { + this.id = set.getInt("id"); + this.userId = set.getInt("user_id"); + this.badgeId = set.getString("badge_id"); + this.badgeName = set.getString("badge_name"); + this.badgeDescription = set.getString("badge_description"); + this.dateCreated = set.getInt("date_created"); + this.dateEdit = set.getInt("date_edit"); + } + + public CustomBadge(int id, int userId, String badgeId, String badgeName, String badgeDescription, int dateCreated, int dateEdit) { + this.id = id; + this.userId = userId; + this.badgeId = badgeId; + this.badgeName = badgeName; + this.badgeDescription = badgeDescription; + this.dateCreated = dateCreated; + this.dateEdit = dateEdit; + } + + public int getId() { + return this.id; + } + + public int getUserId() { + return this.userId; + } + + public String getBadgeId() { + return this.badgeId; + } + + public String getBadgeName() { + return this.badgeName; + } + + public String getBadgeDescription() { + return this.badgeDescription; + } + + public int getDateCreated() { + return this.dateCreated; + } + + public int getDateEdit() { + return this.dateEdit; + } + + public void setBadgeName(String badgeName) { + this.badgeName = badgeName; + } + + public void setBadgeDescription(String badgeDescription) { + this.badgeDescription = badgeDescription; + } + + public void setDateEdit(int dateEdit) { + this.dateEdit = dateEdit; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeException.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeException.java new file mode 100644 index 00000000..4a543e06 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeException.java @@ -0,0 +1,15 @@ +package com.eu.habbo.habbohotel.users.custombadge; + +public class CustomBadgeException extends Exception { + + private final String code; + + public CustomBadgeException(String code, String message) { + super(message); + this.code = code; + } + + public String getCode() { + return this.code; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeManager.java new file mode 100644 index 00000000..bc1b2cad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeManager.java @@ -0,0 +1,588 @@ +package com.eu.habbo.habbohotel.users.custombadge; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboBadge; +import com.eu.habbo.habbohotel.users.inventory.BadgesComponent; +import com.eu.habbo.messages.outgoing.inventory.InventoryBadgesComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +public class CustomBadgeManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomBadgeManager.class); + + public static final int MAX_PER_USER = 5; + public static final int BADGE_WIDTH = 40; + public static final int BADGE_HEIGHT = 40; + public static final int MAX_BADGE_SIZE_BYTES = 40960; + + private static final int RANDOM_SUFFIX_LENGTH = 5; + private static final char[] RANDOM_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); + private static final Pattern BADGE_ID_PATTERN = Pattern.compile("^CUST[A-Z0-9]{" + RANDOM_SUFFIX_LENGTH + "}-\\d+$"); + + private static final byte[] PNG_MAGIC = { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + + private static final int RATE_LIMIT_OPS = 5; + private static final long RATE_LIMIT_WINDOW_MS = 60_000L; + + private final SecureRandom random = new SecureRandom(); + private final Map rateBuckets = new ConcurrentHashMap<>(); + private final Map textCache = new ConcurrentHashMap<>(); + private final java.util.concurrent.atomic.AtomicLong textCacheVersion = new java.util.concurrent.atomic.AtomicLong(); + + private volatile CustomBadgeSettings settings; + + public CustomBadgeManager() { + this.reload(); + } + + public static final class BadgeText { + public final String name; + public final String description; + public BadgeText(String name, String description) { + this.name = name == null ? "" : name; + this.description = description == null ? "" : description; + } + } + + public Map getTextCache() { + return java.util.Collections.unmodifiableMap(this.textCache); + } + + public long getTextCacheVersion() { + return this.textCacheVersion.get(); + } + + private void loadTextCache() { + Map next = new java.util.HashMap<>(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT `badge_id`, `badge_name`, `badge_description` FROM `user_custom_badge`")) { + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + next.put(resultSet.getString("badge_id"), + new BadgeText( + resultSet.getString("badge_name"), + resultSet.getString("badge_description"))); + } + } + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to load badge text cache.", e); + return; + } + this.textCache.clear(); + this.textCache.putAll(next); + this.textCacheVersion.incrementAndGet(); + LOGGER.info("CustomBadgeManager -> loaded {} custom badge texts into memory.", next.size()); + } + + public void reload() { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT `badge_path`, `badge_url`, `price_badge`, `currency_type` FROM `users_custom_badge_settings` ORDER BY `id` ASC LIMIT 1")) { + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + this.settings = new CustomBadgeSettings( + resultSet.getString("badge_path"), + resultSet.getString("badge_url"), + resultSet.getInt("price_badge"), + resultSet.getInt("currency_type")); + } else { + this.settings = new CustomBadgeSettings( + "/var/www/gamedata/c_images/album1584", + "/gamedata/c_images/album1584", + 0, -1); + LOGGER.warn("CustomBadgeManager -> No row found in users_custom_badge_settings, falling back to defaults."); + } + } + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to load settings.", e); + } + + loadTextCache(); + } + + public CustomBadgeSettings getSettings() { + return this.settings; + } + + public List listForUser(int userId) { + List result = new ArrayList<>(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT * FROM `user_custom_badge` WHERE `user_id` = ? ORDER BY `date_created` ASC")) { + statement.setInt(1, userId); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + result.add(new CustomBadge(resultSet)); + } + } + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to list badges for user " + userId, e); + } + return result; + } + + public CustomBadge getByBadgeId(String badgeId) { + if (badgeId == null || badgeId.isEmpty()) return null; + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT * FROM `user_custom_badge` WHERE `badge_id` = ? LIMIT 1")) { + statement.setString(1, badgeId); + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return new CustomBadge(resultSet); + } + } + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to load badge " + badgeId, e); + } + return null; + } + + public int countForUser(int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT COUNT(*) FROM `user_custom_badge` WHERE `user_id` = ?")) { + statement.setInt(1, userId); + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + return resultSet.getInt(1); + } + } + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to count badges for user " + userId, e); + } + return 0; + } + + public CustomBadge create(int userId, String name, String description, byte[] pngBytes) throws CustomBadgeException { + enforceRateLimit(userId); + + if (this.countForUser(userId) >= MAX_PER_USER) { + throw new CustomBadgeException("limit_reached", "Maximum of " + MAX_PER_USER + " custom badges reached."); + } + + BufferedImage image = validatePng(pngBytes); + + chargeForCreate(userId); + + String badgeId = generateBadgeId(); + int now = (int) (System.currentTimeMillis() / 1000L); + + try { + writeBadgeFile(badgeId, image); + } catch (CustomBadgeException e) { + refundForCreate(userId); + throw e; + } + + String safeName = sanitize(name, 64); + String safeDesc = sanitize(description, 255); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO `user_custom_badge` (`user_id`, `badge_id`, `badge_name`, `badge_description`, `date_created`, `date_edit`) VALUES (?, ?, ?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, userId); + statement.setString(2, badgeId); + statement.setString(3, safeName); + statement.setString(4, safeDesc); + statement.setInt(5, now); + statement.setInt(6, now); + statement.executeUpdate(); + + int generatedId = 0; + try (ResultSet keys = statement.getGeneratedKeys()) { + if (keys.next()) generatedId = keys.getInt(1); + } + + this.textCache.put(badgeId, new BadgeText(safeName, safeDesc)); + this.textCacheVersion.incrementAndGet(); + issueBadgeToInventory(userId, badgeId); + + return new CustomBadge(generatedId, userId, badgeId, safeName, safeDesc, now, now); + } catch (SQLException e) { + deleteBadgeFileQuietly(badgeId); + refundForCreate(userId); + LOGGER.error("CustomBadgeManager -> Failed to insert badge for user " + userId, e); + throw new CustomBadgeException("db_error", "Could not save the badge."); + } + } + + public CustomBadge update(int userId, String oldBadgeId, String name, String description, byte[] pngBytes) throws CustomBadgeException { + enforceRateLimit(userId); + + CustomBadge existing = getByBadgeId(oldBadgeId); + if (existing == null || existing.getUserId() != userId) { + throw new CustomBadgeException("not_found", "Badge not found."); + } + + BufferedImage image = validatePng(pngBytes); + + String newBadgeId = generateBadgeId(); + int now = (int) (System.currentTimeMillis() / 1000L); + + writeBadgeFile(newBadgeId, image); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE `user_custom_badge` SET `badge_id` = ?, `badge_name` = ?, `badge_description` = ?, `date_edit` = ? WHERE `id` = ?")) { + statement.setString(1, newBadgeId); + statement.setString(2, sanitize(name, 64)); + statement.setString(3, sanitize(description, 255)); + statement.setInt(4, now); + statement.setInt(5, existing.getId()); + statement.executeUpdate(); + } catch (SQLException e) { + deleteBadgeFileQuietly(newBadgeId); + LOGGER.error("CustomBadgeManager -> Failed to update badge " + oldBadgeId, e); + throw new CustomBadgeException("db_error", "Could not update the badge."); + } + + String safeName = sanitize(name, 64); + String safeDesc = sanitize(description, 255); + this.textCache.remove(oldBadgeId); + this.textCache.put(newBadgeId, new BadgeText(safeName, safeDesc)); + this.textCacheVersion.incrementAndGet(); + renameBadgeInInventory(userId, oldBadgeId, newBadgeId); + deleteBadgeFileQuietly(oldBadgeId); + return new CustomBadge(existing.getId(), userId, newBadgeId, safeName, safeDesc, existing.getDateCreated(), now); + } + + public void delete(int userId, String badgeId) throws CustomBadgeException { + enforceRateLimit(userId); + + CustomBadge existing = getByBadgeId(badgeId); + if (existing == null || existing.getUserId() != userId) { + throw new CustomBadgeException("not_found", "Badge not found."); + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `user_custom_badge` WHERE `id` = ?")) { + statement.setInt(1, existing.getId()); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to delete badge " + badgeId, e); + throw new CustomBadgeException("db_error", "Could not delete the badge."); + } + + this.textCache.remove(badgeId); + this.textCacheVersion.incrementAndGet(); + revokeBadgeFromInventory(userId, badgeId); + deleteBadgeFileQuietly(badgeId); + } + + public boolean isCustomBadgeId(String badgeId) { + return badgeId != null && BADGE_ID_PATTERN.matcher(badgeId).matches(); + } + + public String generateBadgeId() { + long timestamp = System.currentTimeMillis() / 1000L; + for (int attempt = 0; attempt < 8; attempt++) { + StringBuilder suffix = new StringBuilder(RANDOM_SUFFIX_LENGTH); + for (int i = 0; i < RANDOM_SUFFIX_LENGTH; i++) { + suffix.append(RANDOM_ALPHABET[this.random.nextInt(RANDOM_ALPHABET.length)]); + } + String candidate = "CUST" + suffix + "-" + timestamp; + if (getByBadgeId(candidate) == null) return candidate; + timestamp++; + } + throw new IllegalStateException("Could not allocate a unique custom badge id after 8 attempts."); + } + + public String publicUrlFor(String badgeId) { + CustomBadgeSettings current = this.settings; + if (current == null) return ""; + String base = current.getBadgeUrl(); + if (base == null || base.isEmpty()) return ""; + if (base.endsWith("/")) return base + badgeId + ".gif"; + return base + "/" + badgeId + ".gif"; + } + + private void chargeForCreate(int userId) throws CustomBadgeException { + CustomBadgeSettings current = this.settings; + if (current == null) return; + int price = current.getPriceBadge(); + if (price <= 0) return; + + Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId); + if (habbo == null) { + throw new CustomBadgeException("must_be_online", + "You must be online in the hotel to create a paid badge."); + } + + int currencyType = current.getCurrencyType(); + if (currencyType == -1) { + if (habbo.getHabboInfo().getCredits() < price) { + throw new CustomBadgeException("insufficient_funds", + "You don't have enough credits (need " + price + ")."); + } + habbo.giveCredits(-price); + } else { + if (habbo.getHabboInfo().getCurrencyAmount(currencyType) < price) { + throw new CustomBadgeException("insufficient_funds", + "You don't have enough of that currency (need " + price + ")."); + } + habbo.givePoints(currencyType, -price); + } + } + + private void issueBadgeToInventory(int userId, String badgeId) { + Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId); + if (online != null) { + BadgesComponent.createBadge(badgeId, online); + if (online.getClient() != null) { + online.getClient().sendResponse(new InventoryBadgesComposer(online)); + } + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO `users_badges` (`user_id`, `slot_id`, `badge_code`) VALUES (?, 0, ?)")) { + statement.setInt(1, userId); + statement.setString(2, badgeId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to issue offline badge " + badgeId + " to user " + userId, e); + } + } + + private void renameBadgeInInventory(int userId, String oldBadgeId, String newBadgeId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE `users_badges` SET `badge_code` = ? WHERE `user_id` = ? AND `badge_code` = ?")) { + statement.setString(1, newBadgeId); + statement.setInt(2, userId); + statement.setString(3, oldBadgeId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("CustomBadgeManager -> Failed to rename badge in users_badges " + oldBadgeId + " -> " + newBadgeId, e); + } + + Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId); + if (online == null) return; + + HabboBadge existing = online.getInventory().getBadgesComponent().getBadge(oldBadgeId); + if (existing != null) existing.setCode(newBadgeId); + + if (online.getClient() != null) { + online.getClient().sendResponse(new InventoryBadgesComposer(online)); + } + } + + private void revokeBadgeFromInventory(int userId, String badgeId) { + BadgesComponent.deleteBadge(userId, badgeId); + + Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId); + if (online == null) return; + + online.getInventory().getBadgesComponent().removeBadge(badgeId); + if (online.getClient() != null) { + online.getClient().sendResponse(new InventoryBadgesComposer(online)); + } + } + + private BufferedImage validatePng(byte[] data) throws CustomBadgeException { + if (data == null || data.length == 0) { + throw new CustomBadgeException("empty", "Badge image is empty."); + } + if (data.length > MAX_BADGE_SIZE_BYTES) { + throw new CustomBadgeException("too_large", "Badge image exceeds " + MAX_BADGE_SIZE_BYTES + " bytes."); + } + + if (data.length < PNG_MAGIC.length) { + throw new CustomBadgeException("invalid_image", "Badge image must be a PNG."); + } + for (int i = 0; i < PNG_MAGIC.length; i++) { + if (data[i] != PNG_MAGIC[i]) { + throw new CustomBadgeException("invalid_image", "Badge image must be a PNG."); + } + } + + try (ImageInputStream peek = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) { + if (peek == null) throw new IOException("no input stream"); + Iterator readers = ImageIO.getImageReaders(peek); + if (!readers.hasNext()) { + throw new CustomBadgeException("invalid_image", "Badge image format not recognised."); + } + ImageReader reader = readers.next(); + try { + reader.setInput(peek, true, true); + int w = reader.getWidth(0); + int h = reader.getHeight(0); + if (w != BADGE_WIDTH || h != BADGE_HEIGHT) { + throw new CustomBadgeException("wrong_dimensions", + "Badge image must be " + BADGE_WIDTH + "x" + BADGE_HEIGHT + " pixels."); + } + } finally { + reader.dispose(); + } + } catch (IOException e) { + throw new CustomBadgeException("invalid_image", "Badge image header could not be read."); + } + + BufferedImage image; + try { + image = ImageIO.read(new ByteArrayInputStream(data)); + } catch (IOException e) { + throw new CustomBadgeException("invalid_image", "Badge image could not be decoded."); + } + if (image == null + || image.getWidth() != BADGE_WIDTH + || image.getHeight() != BADGE_HEIGHT) { + throw new CustomBadgeException("invalid_image", "Badge image could not be decoded."); + } + return image; + } + + private void enforceRateLimit(int userId) throws CustomBadgeException { + long now = System.currentTimeMillis(); + long[] bucket = this.rateBuckets.computeIfAbsent(userId, id -> new long[RATE_LIMIT_OPS]); + synchronized (bucket) { + long oldest = Long.MAX_VALUE; + int oldestIdx = 0; + for (int i = 0; i < bucket.length; i++) { + if (bucket[i] < oldest) { oldest = bucket[i]; oldestIdx = i; } + } + if (oldest > now - RATE_LIMIT_WINDOW_MS) { + throw new CustomBadgeException("rate_limited", + "Too many badge operations. Try again in a moment."); + } + bucket[oldestIdx] = now; + } + } + + private void refundForCreate(int userId) { + CustomBadgeSettings current = this.settings; + if (current == null) return; + int price = current.getPriceBadge(); + if (price <= 0) return; + + Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId); + if (habbo == null) { + LOGGER.warn("CustomBadgeManager -> Could not refund {} (price {}): user offline", userId, price); + return; + } + int currencyType = current.getCurrencyType(); + if (currencyType == -1) habbo.giveCredits(price); + else habbo.givePoints(currencyType, price); + } + + private void writeBadgeFile(String badgeId, BufferedImage source) throws CustomBadgeException { + CustomBadgeSettings current = this.settings; + if (current == null || current.getBadgePath() == null || current.getBadgePath().isEmpty()) { + throw new CustomBadgeException("not_configured", "Custom badge storage path is not configured."); + } + try { + Path dir = Paths.get(current.getBadgePath()).toAbsolutePath(); + Files.createDirectories(dir); + Path target = dir.resolve(badgeId + ".gif"); + + BufferedImage indexed = toIndexedGifImage(source); + if (!ImageIO.write(indexed, "gif", target.toFile())) { + throw new IOException("No GIF ImageWriter available."); + } + + LOGGER.info("CustomBadgeManager -> wrote badge {} ({} bytes) to {}", + badgeId, Files.size(target), target); + } catch (IOException e) { + LOGGER.error("CustomBadgeManager -> Failed to write badge " + badgeId + + " to " + current.getBadgePath(), e); + throw new CustomBadgeException("write_failed", "Could not save the badge file."); + } + } + + private static BufferedImage toIndexedGifImage(BufferedImage source) { + int w = source.getWidth(); + int h = source.getHeight(); + int[] pixels = source.getRGB(0, 0, w, h, null, 0, w); + + Map indexByColor = new LinkedHashMap<>(); + indexByColor.put(0, 0); + + for (int p : pixels) { + int alpha = (p >>> 24) & 0xff; + int key = (alpha < 128) ? 0 : (p | 0xFF000000); + if (key == 0) continue; + if (indexByColor.size() >= 256) break; + indexByColor.computeIfAbsent(key, k -> indexByColor.size()); + } + + int n = indexByColor.size(); + byte[] r = new byte[n]; + byte[] g = new byte[n]; + byte[] b = new byte[n]; + int i = 0; + for (Integer color : indexByColor.keySet()) { + r[i] = (byte) ((color >>> 16) & 0xff); + g[i] = (byte) ((color >>> 8) & 0xff); + b[i] = (byte) (color & 0xff); + i++; + } + + IndexColorModel colorModel = new IndexColorModel(8, n, r, g, b, 0); + BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_INDEXED, colorModel); + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int p = pixels[y * w + x]; + int alpha = (p >>> 24) & 0xff; + int key = (alpha < 128) ? 0 : (p | 0xFF000000); + Integer idx = indexByColor.get(key); + out.getRaster().setSample(x, y, 0, idx == null ? 0 : idx); + } + } + + return out; + } + + private void deleteBadgeFileQuietly(String badgeId) { + CustomBadgeSettings current = this.settings; + if (current == null || current.getBadgePath() == null) return; + File file = new File(current.getBadgePath(), badgeId + ".gif"); + if (file.exists() && !file.delete()) { + LOGGER.warn("CustomBadgeManager -> Could not delete stale badge file: {}", file.getAbsolutePath()); + } + } + + private static String sanitize(String value, int maxLength) { + if (value == null) return ""; + StringBuilder out = new StringBuilder(Math.min(value.length(), maxLength)); + for (int i = 0; i < value.length() && out.length() < maxLength; i++) { + char c = value.charAt(i); + if (c < 0x20 || c == 0x7F) continue; + out.append(c); + } + return out.toString().trim(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeSettings.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeSettings.java new file mode 100644 index 00000000..b7b82f86 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeSettings.java @@ -0,0 +1,32 @@ +package com.eu.habbo.habbohotel.users.custombadge; + +public class CustomBadgeSettings { + + private final String badgePath; + private final String badgeUrl; + private final int priceBadge; + private final int currencyType; + + public CustomBadgeSettings(String badgePath, String badgeUrl, int priceBadge, int currencyType) { + this.badgePath = badgePath; + this.badgeUrl = badgeUrl; + this.priceBadge = priceBadge; + this.currencyType = currencyType; + } + + public String getBadgePath() { + return this.badgePath; + } + + public String getBadgeUrl() { + return this.badgeUrl; + } + + public int getPriceBadge() { + return this.priceBadge; + } + + public int getCurrencyType() { + return this.currencyType; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java index 763bcb59..79283cc2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java @@ -9,10 +9,12 @@ public class ChangeInfostandBgEvent extends MessageHandler { int backgroundImage = this.packet.readInt(); int backgroundStand = this.packet.readInt(); int backgroundOverlay = this.packet.readInt(); + int backgroundCard = this.packet.bytesAvailable() >= 4 ? this.packet.readInt() : 0; this.client.getHabbo().getHabboInfo().setInfostandBg(backgroundImage); this.client.getHabbo().getHabboInfo().setInfostandStand(backgroundStand); this.client.getHabbo().getHabboInfo().setInfostandOverlay(backgroundOverlay); + this.client.getHabbo().getHabboInfo().setInfostandCardBg(backgroundCard); this.client.getHabbo().getHabboInfo().run(); if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java index 2820706a..207a65fa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java @@ -36,6 +36,7 @@ public class RoomPetComposer extends MessageComposer implements TIntObjectProced this.response.appendInt(0); this.response.appendInt(0); this.response.appendInt(0); + this.response.appendInt(0); if (pet instanceof IPetLook) { this.response.appendString(((IPetLook) pet).getLook()); } else { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java index c1024c8f..755fdcc9 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java @@ -23,6 +23,7 @@ public class RoomUserDataComposer extends MessageComposer { this.response.appendInt(this.habbo.getHabboInfo().getInfostandBg()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay()); + this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg()); return this.response; } 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 9d634f44..cf878af8 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 @@ -43,6 +43,7 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendInt(this.habbo.getHabboInfo().getInfostandBg()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay()); + this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg()); this.response.appendString(this.habbo.getHabboInfo().getLook()); this.response.appendInt(this.habbo.getRoomUnit().getId()); //Room Unit ID this.response.appendInt(this.habbo.getRoomUnit().getX()); @@ -78,6 +79,7 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendInt(habbo.getHabboInfo().getInfostandBg()); this.response.appendInt(habbo.getHabboInfo().getInfostandStand()); this.response.appendInt(habbo.getHabboInfo().getInfostandOverlay()); + this.response.appendInt(habbo.getHabboInfo().getInfostandCardBg()); this.response.appendString(habbo.getHabboInfo().getLook()); this.response.appendInt(habbo.getRoomUnit().getId()); //Room Unit ID this.response.appendInt(habbo.getRoomUnit().getX()); @@ -111,6 +113,7 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendInt(0); this.response.appendInt(0); this.response.appendInt(0); + this.response.appendInt(0); this.response.appendString(this.bot.getFigure()); this.response.appendInt(this.bot.getRoomUnit().getId()); this.response.appendInt(this.bot.getRoomUnit().getX()); @@ -143,6 +146,7 @@ public class RoomUsersComposer extends MessageComposer { 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()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java index 5dd3e809..0a6fb0e7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java @@ -115,6 +115,7 @@ public class UserProfileComposer extends MessageComposer { this.response.appendInt(this.habboInfo.getInfostandBg()); this.response.appendInt(this.habboInfo.getInfostandStand()); this.response.appendInt(this.habboInfo.getInfostandOverlay()); + this.response.appendInt(this.habboInfo.getInfostandCardBg()); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java index dfba3f75..4558d9af 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java @@ -3,6 +3,7 @@ package com.eu.habbo.networking.gameserver; import com.eu.habbo.Emulator; import com.eu.habbo.messages.PacketManager; import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler; +import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler; import com.eu.habbo.networking.gameserver.codec.WebSocketCodec; import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler; import com.eu.habbo.networking.gameserver.decoders.*; @@ -53,6 +54,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer 128) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing or invalid ssoTicket.")); + return; + } + + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement lookup = conn.prepareStatement( + "SELECT id, username FROM users WHERE auth_ticket = ? LIMIT 1")) { + lookup.setString(1, ssoTicket); + try (ResultSet rs = lookup.executeQuery()) { + if (!rs.next()) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("SSO ticket not recognised.")); + return; + } + int userId = rs.getInt("id"); + String username = rs.getString("username"); + + AuthRateLimiter.recordSuccess(ip); + + AccessTokenService.Issued access = AccessTokenService.issue(userId); + JsonObject ok = new JsonObject(); + ok.addProperty("username", username); + ok.addProperty("accessToken", access.token); + ok.addProperty("accessTokenExpiresAt", access.expiresAt); + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } + } catch (Exception e) { + LOGGER.error("[auth/sso-token] lookup failed", e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error.")); + } + } + private void handleRefresh(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { String jwt = readString(body, "rememberToken").trim(); if (jwt.isEmpty()) { @@ -365,6 +410,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { JsonObject ok = new JsonObject(); ok.addProperty("rememberToken", rot.jwt); ok.addProperty("expiresAt", rot.expiresAt); + AccessTokenService.Issued access = AccessTokenService.issue(rot.userId); + ok.addProperty("accessToken", access.token); + ok.addProperty("accessTokenExpiresAt", access.expiresAt); sendJson(ctx, req, HttpResponseStatus.OK, ok); } catch (Exception e) { LOGGER.error("Refresh failed", e); @@ -456,6 +504,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { ok.addProperty("ssoTicket", ssoTicket); ok.addProperty("username", rs.getString("username")); if (rememberToken != null) ok.addProperty("rememberToken", rememberToken); + AccessTokenService.Issued access = AccessTokenService.issue(userId); + ok.addProperty("accessToken", access.token); + ok.addProperty("accessTokenExpiresAt", access.expiresAt); sendJson(ctx, req, HttpResponseStatus.OK, ok); } } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/badges/BadgeHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/badges/BadgeHttpHandler.java new file mode 100644 index 00000000..e06b9cae --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/badges/BadgeHttpHandler.java @@ -0,0 +1,371 @@ +package com.eu.habbo.networking.gameserver.badges; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.custombadge.CustomBadge; +import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeException; +import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager; +import com.eu.habbo.networking.gameserver.GameServerAttributes; +import com.eu.habbo.networking.gameserver.auth.AccessTokenService; +import com.eu.habbo.networking.gameserver.auth.AuthRateLimiter; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.*; +import io.netty.util.ReferenceCountUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +public class BadgeHttpHandler extends ChannelInboundHandlerAdapter { + + private static final Logger LOGGER = LoggerFactory.getLogger(BadgeHttpHandler.class); + + private static final String BASE_PATH = "/api/badges/custom"; + private static final int MAX_BODY_BYTES = 128 * 1024; + + private static volatile JsonObject cachedTextsResponse = null; + private static volatile long cachedTextsVersion = -1L; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof FullHttpRequest req)) { + super.channelRead(ctx, msg); + return; + } + + String path = new QueryStringDecoder(req.uri()).path(); + if (!path.equals(BASE_PATH) && !path.startsWith(BASE_PATH + "/")) { + super.channelRead(ctx, msg); + return; + } + + try { + handle(ctx, req, path); + } finally { + ReferenceCountUtil.release(req); + } + } + + private void handle(ChannelHandlerContext ctx, FullHttpRequest req, String path) { + if (req.method() == HttpMethod.OPTIONS) { + sendCors(ctx, req); + return; + } + + if (path.equals(BASE_PATH + "/texts")) { + if (req.method() == HttpMethod.GET || req.method() == HttpMethod.HEAD) { + String ip = resolveClientIp(ctx, req); + if (!AuthRateLimiter.tryProbe(ip)) { + long secs = AuthRateLimiter.secondsUntilProbeReset(ip); + sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS, + error("Too many requests. Try again in " + secs + "s.")); + return; + } + handleTexts(ctx, req); + return; + } + sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET.")); + return; + } + + int userId = authenticate(req); + if (userId == 0) { + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, error("Authentication required.")); + return; + } + + if (req.content().readableBytes() > MAX_BODY_BYTES) { + sendJson(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, error("Payload too large.")); + return; + } + + String trailing = path.length() > BASE_PATH.length() ? path.substring(BASE_PATH.length() + 1) : ""; + + try { + if (trailing.isEmpty()) { + if (req.method() == HttpMethod.GET || req.method() == HttpMethod.HEAD) { + handleList(ctx, req, userId); + return; + } + if (req.method() == HttpMethod.POST) { + handleCreate(ctx, req, userId); + return; + } + sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET or POST.")); + return; + } + + String badgeId = trailing; + CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager(); + if (!manager.isCustomBadgeId(badgeId)) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid badge id.")); + return; + } + + if (req.method() == HttpMethod.PUT || req.method() == HttpMethod.POST) { + handleUpdate(ctx, req, userId, badgeId); + return; + } + if (req.method() == HttpMethod.DELETE) { + handleDelete(ctx, req, userId, badgeId); + return; + } + sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use PUT or DELETE.")); + } catch (Exception e) { + LOGGER.error("[badges/custom] unexpected error path=" + path, e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, error("Server error.")); + } + } + + private void handleTexts(ChannelHandlerContext ctx, FullHttpRequest req) { + CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager(); + long version = manager.getTextCacheVersion(); + JsonObject ok = cachedTextsResponse; + if (ok == null || cachedTextsVersion != version) { + java.util.Map cache = manager.getTextCache(); + JsonObject texts = new JsonObject(); + for (java.util.Map.Entry entry : cache.entrySet()) { + String badgeId = entry.getKey(); + CustomBadgeManager.BadgeText value = entry.getValue(); + texts.addProperty("badge_name_" + badgeId, value.name); + texts.addProperty("badge_desc_" + badgeId, value.description); + } + JsonObject built = new JsonObject(); + built.add("texts", texts); + built.addProperty("count", cache.size()); + built.addProperty("version", version); + cachedTextsResponse = built; + cachedTextsVersion = version; + ok = built; + } + sendJsonCached(ctx, req, HttpResponseStatus.OK, ok); + } + + private void handleList(ChannelHandlerContext ctx, FullHttpRequest req, int userId) { + CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager(); + List badges = manager.listForUser(userId); + + JsonArray arr = new JsonArray(); + for (CustomBadge b : badges) arr.add(toJson(b, manager)); + + JsonObject ok = new JsonObject(); + ok.add("badges", arr); + ok.addProperty("max", CustomBadgeManager.MAX_PER_USER); + ok.addProperty("badgeWidth", CustomBadgeManager.BADGE_WIDTH); + ok.addProperty("badgeHeight", CustomBadgeManager.BADGE_HEIGHT); + ok.addProperty("maxBadgeSizeBytes", CustomBadgeManager.MAX_BADGE_SIZE_BYTES); + if (manager.getSettings() != null) { + ok.addProperty("priceBadge", manager.getSettings().getPriceBadge()); + ok.addProperty("currencyType", manager.getSettings().getCurrencyType()); + } + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } + + private void handleCreate(ChannelHandlerContext ctx, FullHttpRequest req, int userId) { + JsonObject body = readJsonBody(req); + if (body == null) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid JSON body.")); + return; + } + + byte[] png = decodeImage(body); + if (png == null) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Missing or invalid image.")); + return; + } + + String name = optString(body, "name"); + String description = optString(body, "description"); + + CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager(); + try { + CustomBadge created = manager.create(userId, name, description, png); + sendJson(ctx, req, HttpResponseStatus.CREATED, toJson(created, manager)); + } catch (CustomBadgeException e) { + sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode())); + } + } + + private void handleUpdate(ChannelHandlerContext ctx, FullHttpRequest req, int userId, String badgeId) { + JsonObject body = readJsonBody(req); + if (body == null) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid JSON body.")); + return; + } + + byte[] png = decodeImage(body); + if (png == null) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Missing or invalid image.")); + return; + } + + String name = optString(body, "name"); + String description = optString(body, "description"); + + CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager(); + try { + CustomBadge updated = manager.update(userId, badgeId, name, description, png); + sendJson(ctx, req, HttpResponseStatus.OK, toJson(updated, manager)); + } catch (CustomBadgeException e) { + sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode())); + } + } + + private void handleDelete(ChannelHandlerContext ctx, FullHttpRequest req, int userId, String badgeId) { + CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager(); + try { + manager.delete(userId, badgeId); + JsonObject ok = new JsonObject(); + ok.addProperty("deleted", badgeId); + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } catch (CustomBadgeException e) { + sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode())); + } + } + + private static byte[] decodeImage(JsonObject body) { + if (!body.has("image")) return null; + try { + String raw = body.get("image").getAsString(); + if (raw == null || raw.isEmpty()) return null; + int comma = raw.indexOf(','); + String b64 = raw.startsWith("data:") && comma >= 0 ? raw.substring(comma + 1) : raw; + return Base64.getDecoder().decode(b64.replaceAll("\\s+", "")); + } catch (Exception e) { + return null; + } + } + + private static JsonObject readJsonBody(FullHttpRequest req) { + try { + String text = req.content().toString(StandardCharsets.UTF_8); + if (text.isEmpty()) return new JsonObject(); + return JsonParser.parseString(text).getAsJsonObject(); + } catch (Exception e) { + return null; + } + } + + private static String optString(JsonObject body, String key) { + if (body == null || !body.has(key) || body.get(key).isJsonNull()) return ""; + try { return body.get(key).getAsString(); } + catch (Exception e) { return ""; } + } + + private static int authenticate(FullHttpRequest req) { + String header = req.headers().get(HttpHeaderNames.AUTHORIZATION); + if (header == null || header.isEmpty()) return 0; + String token; + if (header.startsWith("Bearer ")) token = header.substring(7).trim(); + else token = header.trim(); + return AccessTokenService.verify(token); + } + + private static HttpResponseStatus statusFor(CustomBadgeException e) { + return switch (e.getCode()) { + case "not_found" -> HttpResponseStatus.NOT_FOUND; + case "insufficient_funds" -> HttpResponseStatus.PAYMENT_REQUIRED; + case "must_be_online" -> HttpResponseStatus.CONFLICT; + case "rate_limited" -> HttpResponseStatus.TOO_MANY_REQUESTS; + case "limit_reached", "wrong_dimensions", "too_large", "empty", "invalid_image", "not_configured" -> + HttpResponseStatus.BAD_REQUEST; + default -> HttpResponseStatus.INTERNAL_SERVER_ERROR; + }; + } + + private static JsonObject toJson(CustomBadge badge, CustomBadgeManager manager) { + JsonObject obj = new JsonObject(); + obj.addProperty("badgeId", badge.getBadgeId()); + obj.addProperty("badgeCode", badge.getBadgeId()); + obj.addProperty("name", badge.getBadgeName()); + obj.addProperty("description", badge.getBadgeDescription()); + obj.addProperty("dateCreated", badge.getDateCreated()); + obj.addProperty("dateEdit", badge.getDateEdit()); + obj.addProperty("url", manager.publicUrlFor(badge.getBadgeId())); + return obj; + } + + private static JsonObject error(String message) { + return error(message, null); + } + + private static JsonObject error(String message, String code) { + JsonObject obj = new JsonObject(); + obj.addProperty("error", message); + if (code != null) obj.addProperty("code", code); + return obj; + } + + private static void sendJsonCached(ChannelHandlerContext ctx, FullHttpRequest req, + HttpResponseStatus status, JsonObject body) { + byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=30"); + applyCors(req, response); + boolean keepAlive = isKeepAlive(req); + if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + var future = ctx.writeAndFlush(response); + if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + + private static void sendJson(ChannelHandlerContext ctx, FullHttpRequest req, + HttpResponseStatus status, JsonObject body) { + byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + applyCors(req, response); + boolean keepAlive = isKeepAlive(req); + if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + var future = ctx.writeAndFlush(response); + if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + + private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT); + applyCors(req, response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + private static void applyCors(FullHttpRequest req, FullHttpResponse response) { + String origin = req.headers().get(HttpHeaderNames.ORIGIN); + if (origin != null && !origin.isEmpty()) { + response.headers().set("Access-Control-Allow-Origin", origin); + response.headers().set("Vary", "Origin"); + response.headers().set("Access-Control-Allow-Credentials", "true"); + } + response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS"); + response.headers().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); + } + + private static boolean isKeepAlive(FullHttpRequest req) { + String connection = req.headers().get(HttpHeaderNames.CONNECTION); + if (connection != null && connection.equalsIgnoreCase("close")) return false; + if (connection != null && connection.equalsIgnoreCase("keep-alive")) return true; + return req.protocolVersion().isKeepAliveDefault(); + } + + @SuppressWarnings("unused") + private static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) { + if (ctx.channel().attr(GameServerAttributes.WS_IP).get() != null) { + return ctx.channel().attr(GameServerAttributes.WS_IP).get(); + } + if (ctx.channel().remoteAddress() instanceof InetSocketAddress addr) { + return addr.getAddress().getHostAddress(); + } + return ""; + } +} diff --git a/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar index 5eb2a3f1..1de4428a 100644 Binary files a/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar and b/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar differ