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