Compare commits

...

12 Commits

Author SHA1 Message Date
github-actions[bot] 510e0d082e 🆙 Bump version to 4.2.44 [skip ci] 2026-06-12 13:53:22 +00:00
DuckieTM e13c7fdbb6 Merge pull request #168 from hotellidev/multicolorfurnifix
Fix multicolor furni in furni editor
2026-06-12 15:52:23 +02:00
hotellidev 2a28fbd2e5 Fix multicolor furni in furni editor 2026-06-11 04:07:22 +03:00
github-actions[bot] cd60cba355 🆙 Bump version to 4.2.43 [skip ci] 2026-06-10 13:32:38 +00:00
DuckieTM e62f461962 Merge pull request #167 from duckietm/dev
㊙️ Security updates
2026-06-10 15:31:38 +02:00
duckietm 7f8c98e4f3 ㊙️ Security updates 2026-06-10 15:31:18 +02:00
github-actions[bot] d95e09e64f 🆙 Bump version to 4.2.42 [skip ci] 2026-06-10 13:10:40 +00:00
DuckieTM ebe0690e46 Merge pull request #166 from duckietm/dev
🆙 Fix multiheight
2026-06-10 15:09:31 +02:00
duckietm 0dda0ae0f7 🆙 Fix multiheight 2026-06-10 15:09:14 +02:00
github-actions[bot] 54ab6613f1 🆙 Bump version to 4.2.41 [skip ci] 2026-06-10 12:18:32 +00:00
DuckieTM 9fda766ba5 Merge pull request #165 from duckietm/dev
🆙 Fix Group Forum buy
2026-06-10 14:17:32 +02:00
duckietm 3da9325344 🆙 Fix Group Forum buy 2026-06-10 14:17:17 +02:00
10 changed files with 87 additions and 32 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId> <groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId> <artifactId>Habbo</artifactId>
<version>4.2.40</version> <version>4.2.44</version>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -1247,6 +1247,11 @@ public class CatalogManager {
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) { if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) {
if (baseItem.getName().equals("guild_forum") && guild.getOwnerId() != habbo.getHabboInfo().getId()) {
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata); InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata);
habboItem.setExtradata(""); habboItem.setExtradata("");
habboItem.needsUpdate(true); habboItem.needsUpdate(true);
@@ -65,9 +65,8 @@ public class InteractionMultiHeight extends HabboItem {
if (this.getBaseItem().getMultiHeights().length > 0) { if (this.getBaseItem().getMultiHeights().length > 0) {
this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length)); this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length));
this.needsUpdate(true); this.needsUpdate(true);
room.updateTiles(room.getLayout().getTilesAt(room.getLayout().getTile(this.getX(), this.getY()), this.getBaseItem().getWidth(), this.getBaseItem().getLength(), this.getRotation())); room.updateItem(this);
room.updateItemState(this); this.updateUnitsOnItem(room);
//room.sendComposer(new UpdateStackHeightComposer(this.getX(), this.getY(), this.getBaseItem().getMultiHeights()[Integer.valueOf(this.getExtradata())] * 256.0D).compose());
} }
} }
} }
@@ -14,11 +14,7 @@ import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.habbohotel.users.HabboInventory; import com.eu.habbo.habbohotel.users.HabboInventory;
import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.habbohotel.users.subscriptions.Subscription;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseFailedComposer; import com.eu.habbo.messages.outgoing.catalog.*;
import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseUnavailableComposer;
import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer;
import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer;
import com.eu.habbo.messages.outgoing.catalog.PurchaseOKComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer; import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer;
@@ -52,15 +48,8 @@ public class CatalogBuyItemEvent extends MessageHandler {
int itemId = this.packet.readInt(); int itemId = this.packet.readInt();
String extraData = this.packet.readString(); String extraData = this.packet.readString();
int count = this.packet.readInt(); int count = this.packet.readInt();
if (count < 1) count = 1;
// Clamp the client-supplied quantity. Without this the club-offer if (count > 100) count = 100;
// branch accumulates cost in plain ints and a huge count overflows
// to a negative total, bypassing the affordability checks and
// CREDITING the buyer (free currency/subscription exploit).
if (count < 1 || count > 100) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
try { try {
if (this.client.getHabbo().getInventory().getItemsComponent().itemCount() > HabboInventory.MAXIMUM_ITEMS) { if (this.client.getHabbo().getInventory().getItemsComponent().itemCount() > HabboInventory.MAXIMUM_ITEMS) {
@@ -212,12 +201,6 @@ public class CatalogBuyItemEvent extends MessageHandler {
else else
item = page.getCatalogItem(itemId); item = page.getCatalogItem(itemId);
// Search-results buy sends the catalog offer_id as itemId
// (FurnitureOffer.offerId is derived from furnidata's
// purchaseOfferId, which matches `catalog_items.offer_id`),
// not the `catalog_items.id` primary key that getCatalogItem
// expects. Fall back to scanning the page for the matching
// offer_id so the search → buy flow works.
if (item == null && !(page instanceof RecentPurchasesLayout)) { if (item == null && !(page instanceof RecentPurchasesLayout)) {
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) { for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
if (candidate != null && candidate.getOfferId() == itemId) { if (candidate != null && candidate.getOfferId() == itemId) {
@@ -226,13 +209,7 @@ public class CatalogBuyItemEvent extends MessageHandler {
} }
} }
} }
// Inventory cap check based on the actual base items the
// purchase will create, not the page layout - bots/pets
// can legitimately live on bundle pages, search results,
// recent-purchases, etc., and the layout-instanceof check
// missed all those paths. Mirrors the bot/pet branches
// inside CatalogManager.purchaseItem (Item.isBot / isPet
// and the same prefix check) so detection stays in sync.
boolean itemHasBot = false; boolean itemHasBot = false;
boolean itemHasPet = false; boolean itemHasPet = false;
@@ -78,12 +78,24 @@ public class FurniDataManager {
try { try {
CachedIndex index = indexFor(source); CachedIndex index = indexFor(source);
// 1. Try exact classname match (preserves *N suffix for multicolor items)
String key = baseClassname(classname); String key = baseClassname(classname);
String byClassname = key != null ? index.byClassname.get(key) : null; String byClassname = key != null ? index.byClassname.get(key) : null;
if (byClassname != null) { if (byClassname != null) {
return new LookupResult(byClassname, diagnostic(source, itemId, classname, "matched_classname")); return new LookupResult(byClassname, diagnostic(source, itemId, classname, "matched_classname"));
} }
// 2. Fallback: try stripped classname (without *N suffix) for items whose
// furnidata entry does not include the color-variant suffix.
String strippedKey = strippedClassname(classname);
if (strippedKey != null && !strippedKey.equals(key)) {
String byStripped = index.byClassname.get(strippedKey);
if (byStripped != null) {
return new LookupResult(byStripped, diagnostic(source, itemId, classname, "matched_classname_stripped"));
}
}
String byId = index.byId.get(itemId); String byId = index.byId.get(itemId);
if (byId != null) { if (byId != null) {
return new LookupResult(byId, diagnostic(source, itemId, classname, "matched_id")); return new LookupResult(byId, diagnostic(source, itemId, classname, "matched_id"));
@@ -258,21 +270,58 @@ public class FurniDataManager {
JsonObject obj = el.getAsJsonObject(); JsonObject obj = el.getAsJsonObject();
if (!obj.has("classname")) continue; if (!obj.has("classname")) continue;
// Try exact match first (preserves *N suffix)
String actual = baseClassname(obj.get("classname").getAsString()); String actual = baseClassname(obj.get("classname").getAsString());
if (wanted.equals(actual)) return obj.toString(); if (wanted.equals(actual)) return obj.toString();
} }
} }
// Fallback: try stripped classname (without *N suffix)
String strippedWanted = strippedClassname(classname);
if (strippedWanted != null && !strippedWanted.equals(wanted)) {
for (String section : SECTIONS) {
if (!root.has(section)) continue;
JsonObject sectionObj = root.getAsJsonObject(section);
if (!sectionObj.has("furnitype")) continue;
JsonArray types = sectionObj.getAsJsonArray("furnitype");
for (JsonElement el : types) {
JsonObject obj = el.getAsJsonObject();
if (!obj.has("classname")) continue;
String actual = strippedClassname(obj.get("classname").getAsString());
if (strippedWanted.equals(actual)) return obj.toString();
}
}
}
return null; return null;
} }
/**
* Normalize a classname for index/lookup.
*
* Preserves the full classname including any {@code *N} color-variant suffix
* so that multicolor items (e.g. {@code rare_dragonlamp*1}, {@code rare_dragonlamp*2})
* each get their own index entry. The stripped variant (without {@code *N}) is
* used as a fallback during lookup for items whose furnidata entry does not
* include the suffix.
*/
private static String baseClassname(String classname) { private static String baseClassname(String classname) {
if (classname == null) return null; if (classname == null) return null;
String base = classname.trim().toLowerCase(java.util.Locale.ROOT);
return base.isEmpty() ? null : base;
}
/**
* Like {@link #baseClassname(String)} but strips any trailing {@code *N}
* color-variant suffix. Used as a fallback during lookup.
*/
private static String strippedClassname(String classname) {
if (classname == null) return null;
int star = classname.indexOf('*'); int star = classname.indexOf('*');
String base = star >= 0 ? classname.substring(0, star) : classname; String base = star >= 0 ? classname.substring(0, star) : classname;
base = base.trim().toLowerCase(java.util.Locale.ROOT); base = base.trim().toLowerCase(java.util.Locale.ROOT);
return base.isEmpty() ? null : base; return base.isEmpty() ? null : base;
} }
@@ -25,6 +25,11 @@ public class ToggleFloorItemEvent extends MessageHandler {
private static HashSet<String> PET_BOXES = new HashSet<>(Arrays.asList("val11_present", "gnome_box", "leprechaun_box", "velociraptor_egg", "pterosaur_egg", "petbox_epic")); private static HashSet<String> PET_BOXES = new HashSet<>(Arrays.asList("val11_present", "gnome_box", "leprechaun_box", "velociraptor_egg", "pterosaur_egg", "petbox_epic"));
@Override
public int getRatelimit() {
return 100;
}
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
try { try {
@@ -9,6 +9,11 @@ import com.eu.habbo.plugin.Event;
import com.eu.habbo.plugin.events.furniture.FurnitureToggleEvent; import com.eu.habbo.plugin.events.furniture.FurnitureToggleEvent;
public class ToggleWallItemEvent extends MessageHandler { public class ToggleWallItemEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 100;
}
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
@@ -6,6 +6,11 @@ import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
public class TriggerColorWheelEvent extends MessageHandler { public class TriggerColorWheelEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 100;
}
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
int itemId = this.packet.readInt(); int itemId = this.packet.readInt();
@@ -7,6 +7,11 @@ import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
public class TriggerDiceEvent extends MessageHandler { public class TriggerDiceEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 100;
}
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
int itemId = this.packet.readInt(); int itemId = this.packet.readInt();
@@ -5,6 +5,11 @@ import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
public class TriggerOneWayGateEvent extends MessageHandler { public class TriggerOneWayGateEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 100;
}
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() == null) if (this.client.getHabbo().getHabboInfo().getCurrentRoom() == null)