diff --git a/Database Updates/UpdateDatabase_to_4-0-1.sql b/Database Updates/07012026_UpdateDatabase_to_4-0-1.sql similarity index 100% rename from Database Updates/UpdateDatabase_to_4-0-1.sql rename to Database Updates/07012026_UpdateDatabase_to_4-0-1.sql diff --git a/Database Updates/UpdateDatabase_to_4-0-2.sql b/Database Updates/09012026_UpdateDatabase_to_4-0-2.sql similarity index 100% rename from Database Updates/UpdateDatabase_to_4-0-2.sql rename to Database Updates/09012026_UpdateDatabase_to_4-0-2.sql diff --git a/Database Updates/12012026_Battle Banzai.sql b/Database Updates/12012026_Battle Banzai.sql new file mode 100644 index 00000000..1e9c9461 --- /dev/null +++ b/Database Updates/12012026_Battle Banzai.sql @@ -0,0 +1,18 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ===================================================== +-- Update 4.0.2-beta to 4.0.3-beta +-- ===================================================== + +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +-- Maximum pending flood-fill tasks in the executor queue +-- Prevents memory leaks from rapid tile locking +('hotel.banzai.fill.max_queue', '50'), + +-- Minimum interval (ms) between flood-fill calculations per game +-- Prevents errors via rapid wired triggering +('hotel.banzai.fill.cooldown_ms', '100') +ON DUPLICATE KEY UPDATE `value` = VALUES(`value`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/Database Updates/12012026_Breeding Fixes.sql b/Database Updates/12012026_Breeding Fixes.sql new file mode 100644 index 00000000..52825f2b --- /dev/null +++ b/Database Updates/12012026_Breeding Fixes.sql @@ -0,0 +1,216 @@ +-- ===================================================== +-- Pet Breeding Complete Setup +-- ===================================================== +-- This file sets up all breeding-related data: +-- 1. pet_breeding - Maps parent pet types to offspring types +-- 2. pet_breeding_races - Defines possible breeds/colors for offspring by rarity +-- ===================================================== + +-- ===================================================== +-- SECTION 1: Pet Breeding (Parent -> Offspring Mapping) +-- ===================================================== +-- This table maps which pet type produces which baby type + +CREATE TABLE IF NOT EXISTS `pet_breeding` ( + `pet_id` int(11) NOT NULL COMMENT 'Parent pet type', + `offspring_id` int(11) NOT NULL COMMENT 'Baby pet type', + PRIMARY KEY (`pet_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Clear existing data +TRUNCATE TABLE `pet_breeding`; + +-- Insert breeding mappings +INSERT INTO `pet_breeding` (`pet_id`, `offspring_id`) VALUES +(0, 29), -- Dog -> Baby Dog +(1, 28), -- Cat -> Baby Cat +(3, 25), -- Terrier -> Baby Terrier +(4, 24), -- Bear -> Baby Bear +(5, 30); -- Pig -> Baby Pig + +-- ===================================================== +-- SECTION 2: Pet Breeding Races (Offspring Breeds by Rarity) +-- ===================================================== +-- rarity_level: 1=Common, 2=Uncommon, 3=Rare, 4=Epic +-- breed: The visual breed/color variant of the baby pet +-- +-- Higher rarity = harder to get, more special colors +-- Each baby pet type should have breeds at all 4 rarity levels + +CREATE TABLE IF NOT EXISTS `pet_breeding_races` ( + `pet_id` int(11) NOT NULL COMMENT 'Baby pet type (offspring)', + `rarity_level` int(11) NOT NULL COMMENT '1=Common, 2=Uncommon, 3=Rare, 4=Epic', + `breed` int(11) NOT NULL COMMENT 'Visual breed/color variant', + PRIMARY KEY (`pet_id`, `rarity_level`, `breed`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Clear existing data +TRUNCATE TABLE `pet_breeding_races`; + +-- ===================================================== +-- Baby Dog (29) - Offspring of Dog (0) - 20 breeds +-- ===================================================== +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +-- Common breeds (rarity 1) - Most likely to get +(29, 1, 0), +(29, 1, 1), +(29, 1, 2), +(29, 1, 3), +(29, 1, 4), +(29, 1, 5), +(29, 1, 6), +(29, 1, 7), +-- Uncommon breeds (rarity 2) +(29, 2, 8), +(29, 2, 9), +(29, 2, 10), +(29, 2, 11), +(29, 2, 12), +-- Rare breeds (rarity 3) +(29, 3, 13), +(29, 3, 14), +(29, 3, 15), +(29, 3, 16), +-- Epic breeds (rarity 4) - Hardest to get +(29, 4, 17), +(29, 4, 18), +(29, 4, 19); + +-- ===================================================== +-- Baby Cat (28) - Offspring of Cat (1) - 20 breeds +-- ===================================================== +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +-- Common breeds (rarity 1) +(28, 1, 0), +(28, 1, 1), +(28, 1, 2), +(28, 1, 3), +(28, 1, 4), +(28, 1, 5), +(28, 1, 6), +(28, 1, 7), +-- Uncommon breeds (rarity 2) +(28, 2, 8), +(28, 2, 9), +(28, 2, 10), +(28, 2, 11), +(28, 2, 12), +-- Rare breeds (rarity 3) +(28, 3, 13), +(28, 3, 14), +(28, 3, 15), +(28, 3, 16), +-- Epic breeds (rarity 4) +(28, 4, 17), +(28, 4, 18), +(28, 4, 19); + +-- ===================================================== +-- Baby Terrier (25) - Offspring of Terrier (3) - 20 breeds +-- ===================================================== +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +-- Common breeds (rarity 1) +(25, 1, 0), +(25, 1, 1), +(25, 1, 2), +(25, 1, 3), +(25, 1, 4), +(25, 1, 5), +(25, 1, 6), +(25, 1, 7), +-- Uncommon breeds (rarity 2) +(25, 2, 8), +(25, 2, 9), +(25, 2, 10), +(25, 2, 11), +(25, 2, 12), +-- Rare breeds (rarity 3) +(25, 3, 13), +(25, 3, 14), +(25, 3, 15), +(25, 3, 16), +-- Epic breeds (rarity 4) +(25, 4, 17), +(25, 4, 18), +(25, 4, 19); + +-- ===================================================== +-- Baby Bear (24) - Offspring of Bear (4) - 20 breeds +-- ===================================================== +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +-- Common breeds (rarity 1) +(24, 1, 0), +(24, 1, 1), +(24, 1, 2), +(24, 1, 3), +(24, 1, 4), +(24, 1, 5), +(24, 1, 6), +(24, 1, 7), +-- Uncommon breeds (rarity 2) +(24, 2, 8), +(24, 2, 9), +(24, 2, 10), +(24, 2, 11), +(24, 2, 12), +-- Rare breeds (rarity 3) +(24, 3, 13), +(24, 3, 14), +(24, 3, 15), +(24, 3, 16), +-- Epic breeds (rarity 4) +(24, 4, 17), +(24, 4, 18), +(24, 4, 19); + +-- ===================================================== +-- Baby Pig (30) - Offspring of Pig (5) - 20 breeds +-- ===================================================== +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +-- Common breeds (rarity 1) +(30, 1, 0), +(30, 1, 1), +(30, 1, 2), +(30, 1, 3), +(30, 1, 4), +(30, 1, 5), +(30, 1, 6), +(30, 1, 7), +-- Uncommon breeds (rarity 2) +(30, 2, 8), +(30, 2, 9), +(30, 2, 10), +(30, 2, 11), +(30, 2, 12), +-- Rare breeds (rarity 3) +(30, 3, 13), +(30, 3, 14), +(30, 3, 15), +(30, 3, 16), +-- Epic breeds (rarity 4) +(30, 4, 17), +(30, 4, 18), +(30, 4, 19); + +-- ===================================================== +-- Also ensure pet_actions has correct offspring_type values +-- ===================================================== +UPDATE `pet_actions` SET `offspring_type` = 29 WHERE `pet_type` = 0; -- Dog -> Baby Dog +UPDATE `pet_actions` SET `offspring_type` = 28 WHERE `pet_type` = 1; -- Cat -> Baby Cat +UPDATE `pet_actions` SET `offspring_type` = 25 WHERE `pet_type` = 3; -- Terrier -> Baby Terrier +UPDATE `pet_actions` SET `offspring_type` = 24 WHERE `pet_type` = 4; -- Bear -> Baby Bear +UPDATE `pet_actions` SET `offspring_type` = 30 WHERE `pet_type` = 5; -- Pig -> Baby Pig + +-- Set non-breedable pets to -1 +UPDATE `pet_actions` SET `offspring_type` = -1 WHERE `pet_type` NOT IN (0, 1, 3, 4, 5); + +-- ===================================================== +-- Fix any items_base with leading/trailing spaces in interaction_type +-- ===================================================== +UPDATE `items_base` SET `interaction_type` = TRIM(`interaction_type`); + +-- ===================================================== +-- Ensure breeding nest items have correct interaction_type +-- ===================================================== +UPDATE `items_base` SET `interaction_type` = 'breeding_nest' +WHERE `item_name` LIKE 'pet_breeding_%' AND `interaction_type` != 'breeding_nest'; diff --git a/Emulator/pom.xml b/Emulator/pom.xml index 0f362d0a..01c985c5 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -6,7 +6,7 @@ com.eu.habbo Habbo - 4.0.2 + 4.0.3 UTF-8 diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index e793299b..50037361 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -38,7 +38,7 @@ public final class Emulator { public final static int MAJOR = 4; public final static int MINOR = 0; - public final static int BUILD = 1; + public final static int BUILD = 3; public final static String PREVIEW = ""; public static final String version = "Arcturus Morningstar" + " " + MAJOR + "." + MINOR + "." + BUILD + " " + PREVIEW; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/games/battlebanzai/BattleBanzaiGame.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/games/battlebanzai/BattleBanzaiGame.java index f509b8e0..fa35fa59 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/games/battlebanzai/BattleBanzaiGame.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/games/battlebanzai/BattleBanzaiGame.java @@ -21,8 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.*; public class BattleBanzaiGame extends Game { private static final Logger LOGGER = LoggerFactory.getLogger(BattleBanzaiGame.class); @@ -39,9 +38,29 @@ public class BattleBanzaiGame extends Game { public static final int POINTS_LOCK_TILE = Emulator.getConfig().getInt("hotel.banzai.points.tile.lock", 1); - private static final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(Emulator.getConfig().getInt("hotel.banzai.fill.threads", 2)); + /** + * Maximum number of pending flood-fill tasks allowed in the queue. + * This prevents memory exhaustion from rapid tile locking via wireds. + */ + private static final int MAX_PENDING_FILL_TASKS = Emulator.getConfig().getInt("hotel.banzai.fill.max_queue", 50); + + /** + * Minimum interval in milliseconds between flood-fill calculations for the same game. + * This prevents abuse via rapid wired triggering. + */ + private static final int FLOOD_FILL_COOLDOWN_MS = Emulator.getConfig().getInt("hotel.banzai.fill.cooldown_ms", 100); + + private static final ThreadPoolExecutor executor = new ThreadPoolExecutor( + Emulator.getConfig().getInt("hotel.banzai.fill.threads", 2), + Emulator.getConfig().getInt("hotel.banzai.fill.threads", 2), + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(MAX_PENDING_FILL_TASKS), + new ThreadPoolExecutor.DiscardOldestPolicy() // Drop oldest task when queue is full + ); + private final THashMap> lockedTiles; private final THashMap gameTiles; + private volatile long lastFloodFillTime = 0; private int tileCount; private int countDown; private int countDown2; @@ -267,13 +286,27 @@ public class BattleBanzaiGame extends Game { if (doNotCheckFill) return; + // Rate limit flood-fill calculations to prevent memory exhaustion from rapid wired triggering + long now = System.currentTimeMillis(); + if (now - this.lastFloodFillTime < FLOOD_FILL_COOLDOWN_MS) { + return; + } + this.lastFloodFillTime = now; + + // Check if executor queue is getting too full (additional safety check) + if (executor.getQueue().size() >= MAX_PENDING_FILL_TASKS - 5) { + LOGGER.warn("Battle Banzai flood-fill queue is nearly full, skipping calculation to prevent memory issues"); + return; + } + final int x = item.getX(); final int y = item.getY(); final List> filledAreas = new ArrayList<>(); final THashSet lockedTiles = new THashSet<>(this.lockedTiles.get(teamColor)); - executor.execute(() -> { + try { + executor.execute(() -> { filledAreas.add(this.floodFill(x, y - 1, lockedTiles, new ArrayList<>(), teamColor)); filledAreas.add(this.floodFill(x, y + 1, lockedTiles, new ArrayList<>(), teamColor)); filledAreas.add(this.floodFill(x - 1, y, lockedTiles, new ArrayList<>(), teamColor)); @@ -299,6 +332,9 @@ public class BattleBanzaiGame extends Game { } } }); + } catch (RejectedExecutionException e) { + LOGGER.warn("Battle Banzai flood-fill task rejected - queue is full"); + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java index c9dd8beb..f6315093 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java @@ -8,13 +8,14 @@ import com.eu.habbo.habbohotel.pets.PetManager; import com.eu.habbo.habbohotel.pets.PetTasks; import com.eu.habbo.habbohotel.pets.RideablePet; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitType; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.rooms.pets.PetPackageNameValidationComposer; import com.eu.habbo.messages.outgoing.rooms.pets.breeding.PetBreedingCompleted; -import com.eu.habbo.messages.outgoing.rooms.pets.breeding.PetBreedingResultComposer; import com.eu.habbo.threading.runnables.QueryDeleteHabboItem; import java.sql.ResultSet; @@ -73,9 +74,11 @@ public class InteractionPetBreedingNest extends HabboItem { Habbo ownerPetTwo = room.getHabbo(this.petTwo.getUserId()); if (ownerPetOne != null && ownerPetTwo != null && this.petOne.getPetData().getType() == this.petTwo.getPetData().getType() && this.petOne.getPetData().getOffspringType() != -1) { - ownerPetTwo.getClient().sendResponse(new PetBreedingResultComposer(this.getId(), this.petOne.getPetData().getOffspringType(), this.petOne, ownerPetOne.getHabboInfo().getUsername(), this.petTwo, ownerPetTwo.getHabboInfo().getUsername())); - this.setExtradata("1"); - room.updateItem(this); + // Auto-breed with generated name (client doesn't have breeding dialog) + String babyName = generateBabyName(this.petOne.getName(), this.petTwo.getName()); + this.breed(ownerPetTwo, babyName, this.petOne.getId(), this.petTwo.getId()); + } else { + this.freePets(); } } } @@ -114,6 +117,25 @@ public class InteractionPetBreedingNest extends HabboItem { return false; } + /** + * Allow pets to walk onto this tile even if another pet is already on it. + * This is required because breeding nests are 1x1 and need 2 pets to breed. + */ + @Override + public boolean canOverrideTile(RoomUnit unit, Room room, RoomTile tile) { + // Only allow override for pets when the box isn't full yet + if (unit.getRoomUnitType() == RoomUnitType.PET && !this.boxFull()) { + Pet pet = room.getPet(unit); + if (pet != null) { + // Make sure it's the right pet type for this nest + if (pet.getPetData() != null && pet.getPetData().getOffspringType() != -1) { + return true; + } + } + } + return false; + } + public void stopBreeding(Habbo habbo) { this.setExtradata("0"); habbo.getHabboInfo().getCurrentRoom().updateItem(this); @@ -146,6 +168,27 @@ public class InteractionPetBreedingNest extends HabboItem { } } + private String generateBabyName(String nameOne, String nameTwo) { + // Take first half of parent 1's name and second half of parent 2's name + int mid1 = Math.max(1, nameOne.length() / 2); + int mid2 = nameTwo.length() / 2; + + String part1 = nameOne.substring(0, mid1); + String part2 = nameTwo.substring(mid2); + + String combined = part1 + part2; + + // Ensure name is between 1 and 15 characters + if (combined.length() > 15) { + combined = combined.substring(0, 15); + } + if (combined.isEmpty()) { + combined = "Baby"; + } + + return combined; + } + public void breed(Habbo habbo, String name, int petOneId, int petTwoId) { Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/MonsterplantPet.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/MonsterplantPet.java index 3dce2a58..e7e14e0f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/MonsterplantPet.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/MonsterplantPet.java @@ -260,6 +260,17 @@ public class MonsterplantPet extends Pet implements IPetLook { public void setDeathTimestamp(int deathTimestamp) { this.deathTimestamp = deathTimestamp; + this.needsUpdate = true; + } + + /** + * Revives a dead monster plant, resetting its death timestamp and hasDied flag. + * Call this instead of just setDeathTimestamp when reviving with mnstr_revival. + */ + public void revive() { + this.deathTimestamp = Emulator.getIntUnixTimestamp() + MonsterplantPet.timeToLive; + this.hasDied = false; // Reset so achievement can trigger again if plant dies again + this.needsUpdate = true; } public int getGrowthStage() { @@ -284,6 +295,7 @@ public class MonsterplantPet extends Pet implements IPetLook { public void setCanBreed(boolean canBreed) { this.canBreed = canBreed; + this.needsUpdate = true; } public boolean breedable() { @@ -296,15 +308,28 @@ public class MonsterplantPet extends Pet implements IPetLook { public void setPubliclyBreedable(boolean isPubliclyBreedable) { this.publiclyBreedable = isPubliclyBreedable; + this.needsUpdate = true; } public void breed(MonsterplantPet pet) { + // Validate both plants can breed + if (!this.breedable() || !pet.breedable()) { + return; + } + if (this.canBreed && pet.canBreed) { this.canBreed = false; this.publiclyBreedable = false; + this.needsUpdate = true; pet.setCanBreed(false); pet.setPubliclyBreedable(false); + // pet.needsUpdate is set by setCanBreed and setPubliclyBreedable + + // Persist changes to database + Emulator.getThreading().run(this); + Emulator.getThreading().run(pet); + this.room.sendComposer(new PetStatusUpdateComposer(pet).compose()); this.room.sendComposer(new PetStatusUpdateComposer(this).compose()); @@ -387,6 +412,8 @@ public class MonsterplantPet extends Pet implements IPetLook { this.setDeathTimestamp(Emulator.getIntUnixTimestamp() + MonsterplantPet.timeToLive); this.addHappiness(10); this.addExperience(10); + // needsUpdate is set by setDeathTimestamp, persist to database + Emulator.getThreading().run(this); this.room.sendComposer(new PetStatusUpdateComposer(this).compose()); this.room.sendComposer(new RoomPetRespectComposer(this, RoomPetRespectComposer.PET_TREATED).compose()); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java index 3eac86e1..e820bed5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java @@ -122,15 +122,29 @@ public class PetManager { } public static int randomBody(int minimumRarity, boolean isRare) { - int randomRarity = isRare ? random(Math.max(minimumRarity - 1, 0), (MonsterplantPet.bodyRarity.size() - minimumRarity) + (minimumRarity - 1), 2.0) : random(Math.max(minimumRarity - 1, 0), MonsterplantPet.bodyRarity.size(), 2.0); + int maxIndex = MonsterplantPet.bodyRarity.size(); + int randomIndex = isRare + ? random(Math.max(minimumRarity - 1, 0), Math.min(maxIndex, (maxIndex - minimumRarity) + minimumRarity), 2.0) + : random(Math.max(minimumRarity - 1, 0), maxIndex, 2.0); + + // Clamp to valid range to prevent ArrayIndexOutOfBoundsException + randomIndex = Math.max(0, Math.min(randomIndex, maxIndex - 1)); - return MonsterplantPet.bodyRarity.get(MonsterplantPet.bodyRarity.keySet().toArray()[randomRarity]).getValue(); + // Return the body type KEY (1-12), not the rarity value + return (Integer) MonsterplantPet.bodyRarity.keySet().toArray()[randomIndex]; } public static int randomColor(int minimumRarity, boolean isRare) { - int randomRarity = isRare ? random(Math.max(minimumRarity - 1, 0), (MonsterplantPet.colorRarity.size() - minimumRarity) + (minimumRarity - 1), 2.0) : random(Math.max(minimumRarity - 1, 0), MonsterplantPet.colorRarity.size(), 2.0); + int maxIndex = MonsterplantPet.colorRarity.size(); + int randomIndex = isRare + ? random(Math.max(minimumRarity - 1, 0), Math.min(maxIndex, (maxIndex - minimumRarity) + minimumRarity), 2.0) + : random(Math.max(minimumRarity - 1, 0), maxIndex, 2.0); + + // Clamp to valid range to prevent ArrayIndexOutOfBoundsException + randomIndex = Math.max(0, Math.min(randomIndex, maxIndex - 1)); - return MonsterplantPet.colorRarity.get(MonsterplantPet.colorRarity.keySet().toArray()[randomRarity]).getValue(); + // Return the color hue KEY (0-10), not the rarity value + return (Integer) MonsterplantPet.colorRarity.keySet().toArray()[randomIndex]; } public static int random(int low, int high, double bias) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/BreedMonsterplantsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/BreedMonsterplantsEvent.java index 9ce44416..7fe68b04 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/BreedMonsterplantsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/BreedMonsterplantsEvent.java @@ -2,24 +2,54 @@ package com.eu.habbo.messages.incoming.rooms.pets; import com.eu.habbo.habbohotel.pets.MonsterplantPet; import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.incoming.MessageHandler; public class BreedMonsterplantsEvent extends MessageHandler { @Override public void handle() throws Exception { - int unknownInt = this.packet.readInt(); //Something state. 2 = accept + int unknownInt = this.packet.readInt(); //Something state. 0 = initiate breeding if (unknownInt == 0) { - Pet petOne = this.client.getHabbo().getHabboInfo().getCurrentRoom().getPet(this.packet.readInt()); - Pet petTwo = this.client.getHabbo().getHabboInfo().getCurrentRoom().getPet(this.packet.readInt()); + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + if (room == null) return; + + Pet petOne = room.getPet(this.packet.readInt()); + Pet petTwo = room.getPet(this.packet.readInt()); if (petOne == null || petTwo == null || petOne == petTwo) { - //TODO Add error return; } if (petOne instanceof MonsterplantPet && petTwo instanceof MonsterplantPet) { - ((MonsterplantPet) petOne).breed((MonsterplantPet) petTwo); + MonsterplantPet plantOne = (MonsterplantPet) petOne; + MonsterplantPet plantTwo = (MonsterplantPet) petTwo; + + // Validate both plants are breedable (fully grown, can breed, not dead) + if (!plantOne.breedable() || !plantTwo.breedable()) { + return; + } + + // Validate ownership - at least one plant must belong to the client + // and the other must be publicly breedable or owned by client + int clientId = this.client.getHabbo().getHabboInfo().getId(); + boolean ownsOne = plantOne.getUserId() == clientId; + boolean ownsTwo = plantTwo.getUserId() == clientId; + + if (!ownsOne && !ownsTwo) { + // Client doesn't own either plant + return; + } + + // If client doesn't own one of them, that one must be publicly breedable + if (!ownsOne && !plantOne.isPubliclyBreedable()) { + return; + } + if (!ownsTwo && !plantTwo.isPubliclyBreedable()) { + return; + } + + plantOne.breed(plantTwo); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/CompostMonsterplantEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/CompostMonsterplantEvent.java index 2b87a45a..5e0bf428 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/CompostMonsterplantEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/CompostMonsterplantEvent.java @@ -23,6 +23,8 @@ public class CompostMonsterplantEvent extends MessageHandler { int petId = this.packet.readInt(); Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + if (room == null) return; + Pet pet = room.getPet(petId); if (pet != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetUseItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetUseItemEvent.java index 126343b9..5a8a3847 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetUseItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetUseItemEvent.java @@ -89,12 +89,23 @@ public class PetUseItemEvent extends MessageHandler { Emulator.getGameEnvironment().getItemManager().deleteItem(item); } } else if (pet instanceof MonsterplantPet) { + // Validate ownership - only owner can use items on their plant + if (pet.getUserId() != this.client.getHabbo().getHabboInfo().getId()) { + return; + } + + MonsterplantPet monsterplant = (MonsterplantPet) pet; + if (item.getBaseItem().getName().equalsIgnoreCase("mnstr_revival")) { - if (((MonsterplantPet) pet).isDead()) { - ((MonsterplantPet) pet).setDeathTimestamp(Emulator.getIntUnixTimestamp() + MonsterplantPet.timeToLive); + if (monsterplant.isDead()) { + // Use revive() method which properly resets hasDied flag and sets needsUpdate + monsterplant.revive(); pet.getRoomUnit().clearStatus(); pet.getRoomUnit().setStatus(RoomUnitStatus.GESTURE, "rev"); - ((MonsterplantPet) pet).packetUpdate = true; + monsterplant.packetUpdate = true; + + // Persist to database + Emulator.getThreading().run(pet); this.client.getHabbo().getHabboInfo().getCurrentRoom().removeHabboItem(item); this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RemoveFloorItemComposer(item).compose()); @@ -106,12 +117,17 @@ public class PetUseItemEvent extends MessageHandler { Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId())); } } else if (item.getBaseItem().getName().equalsIgnoreCase("mnstr_fert")) { - if (!((MonsterplantPet) pet).isFullyGrown()) { + if (!monsterplant.isFullyGrown()) { pet.setCreated(pet.getCreated() - MonsterplantPet.growTime); + pet.needsUpdate = true; pet.getRoomUnit().clearStatus(); pet.cycle(); pet.getRoomUnit().setStatus(RoomUnitStatus.GESTURE, "spd"); - pet.getRoomUnit().setStatus(RoomUnitStatus.fromString("grw" + ((MonsterplantPet) pet).getGrowthStage()), ""); + pet.getRoomUnit().setStatus(RoomUnitStatus.fromString("grw" + monsterplant.getGrowthStage()), ""); + + // Persist to database + Emulator.getThreading().run(pet); + this.client.getHabbo().getHabboInfo().getCurrentRoom().removeHabboItem(item); this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RemoveFloorItemComposer(item).compose()); this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserStatusComposer(pet.getRoomUnit()).compose()); @@ -122,17 +138,20 @@ public class PetUseItemEvent extends MessageHandler { Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId())); } } else if (item.getBaseItem().getName().startsWith("mnstr_rebreed")) { - if (((MonsterplantPet) pet).isFullyGrown() && !((MonsterplantPet) pet).canBreed()) { - if ( - (item.getBaseItem().getName().equalsIgnoreCase("mnstr_rebreed") && ((MonsterplantPet) pet).getRarity() <= 5) || - (item.getBaseItem().getName().equalsIgnoreCase("mnstr_rebreed_2") && ((MonsterplantPet) pet).getRarity() >= 6 && ((MonsterplantPet) pet).getRarity() <= 8) || - (item.getBaseItem().getName().equalsIgnoreCase("mnstr_rebreed_3") && ((MonsterplantPet) pet).getRarity() >= 9) - ) + if (monsterplant.isFullyGrown() && !monsterplant.canBreed() && !monsterplant.isDead()) { + boolean validItem = + (item.getBaseItem().getName().equalsIgnoreCase("mnstr_rebreed") && monsterplant.getRarity() <= 5) || + (item.getBaseItem().getName().equalsIgnoreCase("mnstr_rebreed_2") && monsterplant.getRarity() >= 6 && monsterplant.getRarity() <= 8) || + (item.getBaseItem().getName().equalsIgnoreCase("mnstr_rebreed_3") && monsterplant.getRarity() >= 9); - { - ((MonsterplantPet) pet).setCanBreed(true); + if (validItem) { + // setCanBreed now automatically sets needsUpdate + monsterplant.setCanBreed(true); pet.getRoomUnit().clearStatus(); pet.getRoomUnit().setStatus(RoomUnitStatus.GESTURE, "reb"); + + // Persist to database + Emulator.getThreading().run(pet); this.client.getHabbo().getHabboInfo().getCurrentRoom().removeHabboItem(item); this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RemoveFloorItemComposer(item).compose()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ToggleMonsterplantBreedableEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ToggleMonsterplantBreedableEvent.java index 3cbc2d0d..eafe9fdb 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ToggleMonsterplantBreedableEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ToggleMonsterplantBreedableEvent.java @@ -1,20 +1,39 @@ package com.eu.habbo.messages.incoming.rooms.pets; +import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.pets.MonsterplantPet; import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.rooms.pets.PetStatusUpdateComposer; public class ToggleMonsterplantBreedableEvent extends MessageHandler { @Override public void handle() throws Exception { int petId = this.packet.readInt(); - Pet pet = this.client.getHabbo().getHabboInfo().getCurrentRoom().getPet(petId); + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + if (room == null) return; + + Pet pet = room.getPet(petId); if (pet != null) { if (pet.getUserId() == this.client.getHabbo().getHabboInfo().getId()) { if (pet instanceof MonsterplantPet) { - ((MonsterplantPet) pet).setPubliclyBreedable(((MonsterplantPet) pet).isPubliclyBreedable()); + MonsterplantPet monsterplant = (MonsterplantPet) pet; + + // Only allow toggling if plant is breedable (fully grown, can breed, not dead) + if (monsterplant.breedable()) { + // Toggle the publicly breedable state (was previously setting to same value - bug fix) + monsterplant.setPubliclyBreedable(!monsterplant.isPubliclyBreedable()); + + // Mark for database update + monsterplant.needsUpdate = true; + Emulator.getThreading().run(monsterplant); + + // Send status update to room + room.sendComposer(new PetStatusUpdateComposer(monsterplant).compose()); + } } } } diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java index c8ba4612..da68c787 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/freeze/FreezeHandleSnowballExplosion.java @@ -56,6 +56,9 @@ class FreezeHandleSnowballExplosion implements Runnable { for (RoomTile roomTile : tiles) { THashSet items = this.thrownData.room.getItemsAt(roomTile); + // Track if we already processed a block at this tile to prevent stacking exploit + boolean blockProcessedAtTile = false; + for (HabboItem freezeTile : items) { if (freezeTile instanceof InteractionFreezeTile || freezeTile instanceof InteractionFreezeBlock) { int distance = 0; @@ -93,9 +96,12 @@ class FreezeHandleSnowballExplosion implements Runnable { } } } else if (freezeTile instanceof InteractionFreezeBlock) { - if (freezeTile.getExtradata().equalsIgnoreCase("0")) { + // Only process ONE block per tile to prevent stacking exploit + // Stacking many blocks and exploding them causes massive lag + if (!blockProcessedAtTile && freezeTile.getExtradata().equalsIgnoreCase("0")) { game.explodeBox((InteractionFreezeBlock) freezeTile, distance * 100); player.addScore(FreezeGame.DESTROY_BLOCK_POINTS); + blockProcessedAtTile = true; } } } diff --git a/Latest_Compiled_Version/Habbo-4.0.1-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.0.3-jar-with-dependencies.jar similarity index 94% rename from Latest_Compiled_Version/Habbo-4.0.1-jar-with-dependencies.jar rename to Latest_Compiled_Version/Habbo-4.0.3-jar-with-dependencies.jar index f40dcb4c..1c1abbe0 100644 Binary files a/Latest_Compiled_Version/Habbo-4.0.1-jar-with-dependencies.jar and b/Latest_Compiled_Version/Habbo-4.0.3-jar-with-dependencies.jar differ