🆙 update to 4.0.3

This commit is contained in:
duckietm
2026-01-12 12:03:14 +01:00
parent c650e411da
commit d37a16f4a5
16 changed files with 465 additions and 35 deletions
@@ -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;
@@ -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<GameTeamColors, THashSet<HabboItem>> lockedTiles;
private final THashMap<Integer, HabboItem> 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<List<RoomTile>> filledAreas = new ArrayList<>();
final THashSet<HabboItem> 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");
}
}
}
@@ -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()));
@@ -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());
}
@@ -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) {
@@ -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);
}
}
}
@@ -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) {
@@ -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());
@@ -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());
}
}
}
}
@@ -56,6 +56,9 @@ class FreezeHandleSnowballExplosion implements Runnable {
for (RoomTile roomTile : tiles) {
THashSet<HabboItem> 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;
}
}
}