diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java index 7fd3ee93..7d5ea500 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java @@ -179,9 +179,13 @@ public class BotManager { } public void pickUpBot(Bot bot, Habbo habbo) { - HabboInfo receiverInfo = habbo == null ? Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId()) : habbo.getHabboInfo(); - if (bot != null) { + HabboInfo receiverInfo = resolvePickupReceiver(bot, habbo); + Room botRoom = bot.getRoom(); + if (receiverInfo == null || botRoom == null) { + return; + } + BotPickUpEvent pickedUpEvent = new BotPickUpEvent(bot, habbo); Emulator.getPluginManager().fireEvent(pickedUpEvent); @@ -198,8 +202,8 @@ public class BotManager { return; } - bot.onPickUp(habbo, receiverInfo.getCurrentRoom()); - receiverInfo.getCurrentRoom().removeBot(bot); + bot.onPickUp(habbo, botRoom); + botRoom.removeBot(bot); bot.stopFollowingHabbo(); bot.setOwnerId(receiverInfo.getId()); bot.setOwnerName(receiverInfo.getUsername()); @@ -215,6 +219,14 @@ public class BotManager { } } + private HabboInfo resolvePickupReceiver(Bot bot, Habbo picker) { + if (picker != null && bot.getOwnerId() == picker.getHabboInfo().getId()) { + return picker.getHabboInfo(); + } + + return Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId()); + } + public Bot loadBot(ResultSet set) { try { String type = set.getString("type"); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRentableSpace.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRentableSpace.java index 8bd18bd3..e900e4f6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRentableSpace.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRentableSpace.java @@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.items.interactions; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomLayout; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -133,12 +134,18 @@ public class InteractionRentableSpace extends HabboItem { if (habbo.getHabboStats().isRentingSpace()) return; - if (habbo.getHabboInfo().getCredits() < this.rentCost()) + int cost = this.rentCost(); + boolean hasInfiniteCredits = habbo.hasPermission(Permission.ACC_INFINITE_CREDITS); + if (!hasInfiniteCredits && habbo.getHabboInfo().getCredits() < cost) return; if (habbo.getHabboStats().getClubExpireTimestamp() < Emulator.getIntUnixTimestamp()) return; + if (!hasInfiniteCredits) { + habbo.giveCredits(-cost); + } + this.setRenterId(habbo.getHabboInfo().getId()); this.setRenterName(habbo.getHabboInfo().getUsername()); this.setEndTimestamp(Emulator.getIntUnixTimestamp() + (7 * 86400)); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java index 496d521d..2ab95a0d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java @@ -36,6 +36,10 @@ public class RedeemClothingEvent extends MessageHandler { if (clothing != null) { if (!this.client.getHabbo().getInventory().getWardrobeComponent().getClothing().contains(clothing.id)) { + if (!this.grantClothing(clothing.id)) { + return; + } + item.setRoomId(0); RoomTile tile = this.client.getHabbo().getHabboInfo().getCurrentRoom().getLayout().getTile(item.getX(), item.getY()); this.client.getHabbo().getHabboInfo().getCurrentRoom().removeHabboItem(item); @@ -44,14 +48,6 @@ public class RedeemClothingEvent extends MessageHandler { this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RemoveFloorItemComposer(item, true).compose()); Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId())); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_clothing (user_id, clothing_id) VALUES (?, ?)")) { - statement.setInt(1, this.client.getHabbo().getHabboInfo().getId()); - statement.setInt(2, clothing.id); - statement.execute(); - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - this.client.getHabbo().getInventory().getWardrobeComponent().getClothing().add(clothing.id); this.client.getHabbo().getInventory().getWardrobeComponent().getClothingSets().addAll(clothing.setId); this.client.sendResponse(new UserClothesComposer(this.client.getHabbo())); @@ -67,4 +63,15 @@ public class RedeemClothingEvent extends MessageHandler { } } } + + private boolean grantClothing(int clothingId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_clothing (user_id, clothing_id) VALUES (?, ?)")) { + statement.setInt(1, this.client.getHabbo().getHabboInfo().getId()); + statement.setInt(2, clothingId); + return statement.executeUpdate() > 0; + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + return false; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java index 45098095..8bf4942b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java @@ -103,6 +103,10 @@ public class ToggleFloorItemEvent extends MessageHandler { // Do not move to onClick(). Wired could trigger it. if (item instanceof InteractionMonsterPlantSeed) { + if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId()) { + return; + } + Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId())); boolean isRare = item.getBaseItem().getName().contains("rare"); diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/bots/BotPickupOwnershipContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/bots/BotPickupOwnershipContractTest.java new file mode 100644 index 00000000..43295c39 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/bots/BotPickupOwnershipContractTest.java @@ -0,0 +1,30 @@ +package com.eu.habbo.habbohotel.bots; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class BotPickupOwnershipContractTest { + private static String source() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java")); + } + + @Test + void roomOwnerPickupReturnsBotToOriginalOwner() throws Exception { + String source = source(); + + assertTrue(source.contains("HabboInfo receiverInfo = resolvePickupReceiver(bot, habbo);"), + "bot pickup should resolve the receiver without blindly using the picker"); + assertTrue(source.contains("private HabboInfo resolvePickupReceiver(Bot bot, Habbo picker)"), + "bot pickup receiver logic should be centralized"); + assertTrue(source.contains("return Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId());"), + "when a room owner picks up someone else's bot, it should return to the original bot owner"); + assertTrue(source.contains("Room botRoom = bot.getRoom();"), + "pickup should remove the bot from the bot's current room, not the receiver's current room"); + assertTrue(source.contains("botRoom.removeBot(bot);"), + "bot removal should work even when the original owner is offline"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/RentableSpaceChargeContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/RentableSpaceChargeContractTest.java new file mode 100644 index 00000000..24cb1818 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/RentableSpaceChargeContractTest.java @@ -0,0 +1,31 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RentableSpaceChargeContractTest { + @Test + void rentingSpaceChargesCreditsBeforeMarkingSpaceRented() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRentableSpace.java")); + int rentMethod = source.indexOf("public void rent(Habbo habbo)"); + + assertTrue(rentMethod >= 0, "InteractionRentableSpace must keep explicit rent handling"); + + String rentHandling = source.substring(rentMethod, Math.min(source.length(), rentMethod + 1400)); + + assertTrue(rentHandling.contains("int cost = this.rentCost();"), + "Rent cost must be computed once before charging"); + assertTrue(rentHandling.contains("boolean hasInfiniteCredits = habbo.hasPermission(Permission.ACC_INFINITE_CREDITS);"), + "Renting must honor infinite-credit staff permission before charging"); + assertTrue(rentHandling.contains("!hasInfiniteCredits && habbo.getHabboInfo().getCredits() < cost"), + "Renting must reject non-staff users without enough credits for the computed cost"); + assertTrue(rentHandling.contains("habbo.giveCredits(-cost);"), + "Renting must deduct the computed credit cost"); + assertTrue(rentHandling.indexOf("habbo.giveCredits(-cost);") < rentHandling.indexOf("this.setRenterId"), + "Credits must be charged before the rentable space is marked as rented"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/MonsterPlantSeedOwnershipContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/MonsterPlantSeedOwnershipContractTest.java new file mode 100644 index 00000000..e8516c19 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/MonsterPlantSeedOwnershipContractTest.java @@ -0,0 +1,29 @@ +package com.eu.habbo.messages.incoming.rooms.items; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MonsterPlantSeedOwnershipContractTest { + @Test + void monsterPlantSeedsCanOnlyBeRedeemedByTheirOwner() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java")); + int seedBranch = source.indexOf("item instanceof InteractionMonsterPlantSeed"); + + assertTrue(seedBranch >= 0, "ToggleFloorItemEvent must keep monsterplant seed handling explicit"); + + String seedHandling = source.substring(seedBranch, Math.min(source.length(), seedBranch + 1400)); + + String ownershipGuard = "if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId())"; + + assertTrue(seedHandling.contains(ownershipGuard), + "Monsterplant seed redemption must reject callers who do not own the seed"); + assertTrue(seedHandling.contains("createMonsterplant"), + "Monsterplant seed handling must create the pet inside the guarded branch"); + assertTrue(seedHandling.indexOf(ownershipGuard) < seedHandling.indexOf("createMonsterplant"), + "Ownership rejection must happen before creating the pet"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingContractTest.java new file mode 100644 index 00000000..fcbf35c7 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingContractTest.java @@ -0,0 +1,29 @@ +package com.eu.habbo.messages.incoming.rooms.items; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class RedeemClothingContractTest { + private static String source() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java")); + } + + @Test + void clothingIsGrantedBeforeVoucherFurnitureIsConsumed() throws Exception { + String source = source(); + + int grantCall = source.indexOf("grantClothing("); + int roomRemoval = source.indexOf("removeHabboItem(item)"); + int deleteItem = source.indexOf("new QueryDeleteHabboItem(item.getId())"); + + assertTrue(source.contains("private boolean grantClothing(int clothingId)"), + "clothing DB insert should report whether the grant succeeded"); + assertTrue(grantCall > -1, "redeem path should call grantClothing before consuming the item"); + assertTrue(grantCall < roomRemoval, "room item must not be removed before the clothing grant succeeds"); + assertTrue(grantCall < deleteItem, "voucher furniture must not be deleted before the clothing grant succeeds"); + } +}