Compare commits

...

8 Commits

Author SHA1 Message Date
github-actions[bot] b77290f5e7 🆙 Bump version to 4.2.15 [skip ci] 2026-05-21 15:03:23 +00:00
DuckieTM b14730d37f Merge pull request #118 from duckietm/dev
Dev
2026-05-21 17:02:19 +02:00
duckietm 9126396973 🆙 Fix Catalog Edit 2026-05-21 17:01:56 +02:00
duckietm d321ff3b85 Update 003_live_required_schema.sql 2026-05-21 15:54:10 +02:00
duckietm 7f38a25eef 🆙 Small SQL update 2026-05-21 15:44:30 +02:00
github-actions[bot] 4820ab15f3 🆙 Bump version to 4.2.14 [skip ci] 2026-05-21 12:03:07 +00:00
DuckieTM 8d989e7a19 Merge pull request #117 from duckietm/dev
🆕 Redesign of HC Club buy, now also give as gift
2026-05-21 14:02:14 +02:00
duckietm 1f7ec96e1c 🆕 Redesign of HC Club buy, now also give as gift 2026-05-21 14:01:57 +02:00
9 changed files with 311 additions and 20 deletions
@@ -23,6 +23,10 @@ SET NAMES utf8mb4;
ALTER TABLE `emulator_settings`
ADD COLUMN IF NOT EXISTS `comment` TEXT NULL DEFAULT '' AFTER `value`;
ALTER TABLE catalog_pages
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `includes`;
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
`key` VARCHAR(255) NOT NULL,
`value` TEXT NOT NULL,
+6
View File
@@ -0,0 +1,6 @@
ALTER TABLE catalog_club_offers
ADD COLUMN IF NOT EXISTS giftable ENUM('0','1') NOT NULL DEFAULT '0';
INSERT INTO emulator_texts (`key`, `value`)
VALUES ('prereg.reward.you.received', 'You have recived:'),
('generic.days', 'days');
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.13</version>
<version>4.2.15</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -72,7 +72,11 @@ public class ClubOffer implements ISerialize {
this.type = OfferType.fromDatabase(set.getString("type"));
this.vip = this.type == OfferType.VIP;
this.deal = set.getString("deal").equals("1");
this.giftable = set.getString("giftable").equals("1");
boolean giftable = false;
try {
giftable = "1".equals(set.getString("giftable"));
} catch (SQLException ignored) {}
this.giftable = giftable;
}
public int getId() {
@@ -2,10 +2,8 @@ package com.eu.habbo.messages.incoming.catalog;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.achievements.AchievementManager;
import com.eu.habbo.habbohotel.catalog.CatalogItem;
import com.eu.habbo.habbohotel.catalog.CatalogLimitedConfiguration;
import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.*;
import com.eu.habbo.habbohotel.catalog.layouts.*;
import com.eu.habbo.habbohotel.items.FurnitureType;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.*;
@@ -14,6 +12,7 @@ import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.users.subscriptions.Subscription;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.*;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
@@ -22,16 +21,14 @@ import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
import com.eu.habbo.messages.outgoing.users.UserClubComposer;
import com.eu.habbo.threading.runnables.ShutdownEmulator;
import gnu.trove.map.hash.THashMap;
import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.*;
import java.util.Calendar;
public class CatalogBuyItemAsGiftEvent extends MessageHandler {
@@ -82,6 +79,12 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
int userId = 0;
CatalogPage clubGiftPage = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (this.isClubOfferPage(clubGiftPage)) {
this.handleClubOfferGift(clubGiftPage, itemId, username);
return;
}
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
&& !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
LOGGER.error("DEBUG GIFT: invalid spriteId for gift wrapper/furni -> {}", spriteId);
@@ -554,4 +557,166 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
}
}
private boolean isClubOfferPage(CatalogPage page) {
return page instanceof ClubBuyLayout
|| page instanceof VipBuyLayout
|| page instanceof BuildersClubFrontPageLayout
|| page instanceof BuildersClubAddonsLayout
|| page instanceof BuildersClubLoyaltyLayout;
}
private int getClubOfferWindowId(CatalogPage page) {
if (page instanceof BuildersClubAddonsLayout) {
return ClubOffer.WINDOW_BUILDERS_CLUB_ADDONS;
}
if (page instanceof BuildersClubFrontPageLayout || page instanceof BuildersClubLoyaltyLayout) {
return ClubOffer.WINDOW_BUILDERS_CLUB;
}
return ClubOffer.WINDOW_HABBO_CLUB;
}
private void handleClubOfferGift(CatalogPage page, int offerId, String username) {
ClubOffer offer = Emulator.getGameEnvironment().getCatalogManager().clubOffers.get(offerId);
if (offer == null || !offer.belongsToWindow(this.getClubOfferWindowId(page))) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (!offer.isGiftable()) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
if (offer.isBuildersClubAddon()) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
int totalCredits = offer.getCredits();
int totalPoints = offer.getPoints();
if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(offer.getPointsType())) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
Habbo recipient = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
int recipientId = 0;
if (recipient != null) {
recipientId = recipient.getHabboInfo().getId();
} else {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
statement.setString(1, username);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) recipientId = set.getInt(1);
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while resolving club gift recipient", e);
}
}
if (recipientId == 0) {
this.client.sendResponse(new GiftReceiverNotFoundComposer());
return;
}
if (recipientId == this.client.getHabbo().getHabboInfo().getId()) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
String subscriptionType = offer.isBuildersClubSubscription() ? Subscription.BUILDERS_CLUB : Subscription.HABBO_CLUB;
int duration = offer.getDays() * 86400;
boolean extended;
if (recipient != null) {
extended = (recipient.getHabboStats().createSubscription(subscriptionType, duration) != null);
} else {
extended = this.extendOfflineSubscription(recipientId, subscriptionType, duration);
}
if (!extended) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (totalCredits > 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
this.client.getHabbo().giveCredits(-totalCredits);
}
if (totalPoints > 0) {
if (offer.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
this.client.getHabbo().givePixels(-totalPoints);
} else if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) {
this.client.getHabbo().givePoints(offer.getPointsType(), -totalPoints);
}
}
if (recipient != null) {
recipient.getClient().sendResponse(new UserClubComposer(recipient, subscriptionType, UserClubComposer.RESPONSE_TYPE_NORMAL));
String prefix = Emulator.getTexts().getValue("prereg.reward.you.received", "You have received:");
String daysWord = Emulator.getTexts().getValue("generic.days", "days");
String clubLabel = offer.isBuildersClubSubscription() ? "Builders Club" : "HC";
String giftDescription = clubLabel + " (" + offer.getDays() + " " + daysWord + ")";
THashMap<String, String> keys = new THashMap<>();
keys.put("display", "BUBBLE");
keys.put("image", "${image.library.url}notifications/gift.gif");
keys.put("message", prefix + " " + giftDescription);
recipient.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_GIFT.key, keys));
}
if (this.client.getHabbo().getHabboInfo().getId() != recipientId) {
AchievementManager.progressAchievement(
this.client.getHabbo(),
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver")
);
}
this.client.sendResponse(new PurchaseOKComposer(null));
}
private boolean extendOfflineSubscription(int userId, String subscriptionType, int duration) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement select = connection.prepareStatement(
"SELECT id, duration FROM users_subscriptions WHERE user_id = ? AND subscription_type = ? AND active = 1 ORDER BY id DESC LIMIT 1")) {
select.setInt(1, userId);
select.setString(2, subscriptionType);
try (ResultSet set = select.executeQuery()) {
if (set.next()) {
int subId = set.getInt("id");
int existing = set.getInt("duration");
try (PreparedStatement update = connection.prepareStatement(
"UPDATE users_subscriptions SET duration = ? WHERE id = ?")) {
update.setInt(1, existing + duration);
update.setInt(2, subId);
update.executeUpdate();
return true;
}
}
}
}
try (PreparedStatement insert = connection.prepareStatement(
"INSERT INTO users_subscriptions (user_id, subscription_type, timestamp_start, duration, active) VALUES (?, ?, ?, ?, 1)",
Statement.RETURN_GENERATED_KEYS)) {
insert.setInt(1, userId);
insert.setString(2, subscriptionType);
insert.setInt(3, Emulator.getIntUnixTimestamp());
insert.setInt(4, duration);
insert.executeUpdate();
return true;
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while extending offline subscription", e);
return false;
}
}
}
@@ -36,6 +36,19 @@ public class CatalogAdminCreatePageEvent extends MessageHandler {
pageLayout = CatalogPageLayouts.default_3x3;
}
if (parentId != -1 && Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId) == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
return;
}
if (iconType < 0) iconType = 0;
if (minRank < 1) minRank = 1;
if (orderNum < 0) orderNum = 0;
if (caption == null) caption = "";
if (caption2 == null) caption2 = "";
if (caption.length() > 128) caption = caption.substring(0, 128);
if (caption2.length() > 25) caption2 = caption2.substring(0, 25);
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage(
caption, caption2, 0, iconType, pageLayout, minRank, parentId, pageType, catalogMode
);
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
@@ -11,6 +12,9 @@ import java.sql.PreparedStatement;
public class CatalogAdminMovePageEvent extends MessageHandler {
private static final int MAX_PARENT_WALK = 64;
private static final int ROOT_PARENT_ID = -1;
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
@@ -24,9 +28,7 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages";
// Special values: -1 = toggle enabled, -2 = toggle visible
if (newParentId == -1) {
// Toggle enabled
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) {
@@ -38,7 +40,6 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
}
if (newParentId == -2) {
// Toggle visible
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) {
@@ -49,7 +50,30 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
return;
}
// Normal move: update parent and order
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
if (page == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
if (newParentId == pageId) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "A page cannot be its own parent"));
return;
}
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(newParentId);
if (parent == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + newParentId));
return;
}
if (this.wouldCreateCycle(pageId, newParentId)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to move: that would create a cycle"));
return;
}
if (newIndex < 0) newIndex = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET parent_id = ?, order_num = ? WHERE id = ?")) {
@@ -61,4 +85,16 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page moved"));
}
private boolean wouldCreateCycle(int pageId, int parentId) {
int current = parentId;
for (int hops = 0; hops < MAX_PARENT_WALK; hops++) {
if (current == ROOT_PARENT_ID) return false;
if (current == pageId) return true;
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current);
if (parent == null) return false;
current = parent.getParentId();
}
return true;
}
}
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.CatalogPageLayouts;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
@@ -12,6 +13,14 @@ import java.sql.PreparedStatement;
public class CatalogAdminSavePageEvent extends MessageHandler {
private static final int MAX_CAPTION_LENGTH = 128;
private static final int MAX_CAPTION_SAVE_LENGTH = 25;
private static final int MAX_HEADLINE_LENGTH = 1024;
private static final int MAX_TEASER_LENGTH = 64;
private static final int MAX_TEXT_LENGTH = 8192;
private static final int MAX_PARENT_WALK = 64;
private static final int ROOT_PARENT_ID = -1;
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
@@ -34,7 +43,6 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
String textDetails = this.packet.readString();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
CatalogPageType catalogMode = CatalogPageType.fromString(this.packet.readString());
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
if (page == null) {
@@ -42,6 +50,41 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
return;
}
try {
CatalogPageLayouts.valueOf(layout);
} catch (IllegalArgumentException | NullPointerException e) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid layout: " + layout));
return;
}
if (parentId != ROOT_PARENT_ID) {
if (parentId == pageId) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "A page cannot be its own parent"));
return;
}
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId);
if (parent == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
return;
}
if (this.wouldCreateCycle(pageId, parentId)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to re-parent: that would create a cycle"));
return;
}
}
if (iconType < 0) iconType = 0;
if (minRank < 1) minRank = 1;
if (orderNum < 0) orderNum = 0;
caption = this.clampLength(caption, MAX_CAPTION_LENGTH);
caption2 = this.clampLength(caption2, MAX_CAPTION_SAVE_LENGTH);
headline = this.clampLength(headline, MAX_HEADLINE_LENGTH);
teaser = this.clampLength(teaser, MAX_TEASER_LENGTH);
textDetails = this.clampLength(textDetails, MAX_TEXT_LENGTH);
String query = (pageType == CatalogPageType.BUILDER)
? "UPDATE catalog_pages_bc SET caption = ?, page_layout = ?, icon_image = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ? WHERE id = ?"
: "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, catalog_mode = ? WHERE id = ?";
@@ -82,4 +125,22 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page saved"));
}
private boolean wouldCreateCycle(int pageId, int parentId) {
int current = parentId;
for (int hops = 0; hops < MAX_PARENT_WALK; hops++) {
if (current == ROOT_PARENT_ID) return false;
if (current == pageId) return true;
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current);
if (parent == null) return false;
current = parent.getParentId();
}
return true;
}
private String clampLength(String value, int max) {
if (value == null) return "";
if (value.length() <= max) return value;
return value.substring(0, max);
}
}
@@ -41,6 +41,7 @@ public class CatalogPagesListComposer extends MessageComposer {
this.response.appendBoolean(true);
this.response.appendInt(0);
this.response.appendInt(-1);
this.response.appendInt(-1);
this.response.appendString("root");
this.response.appendString("");
this.response.appendInt(0);
@@ -68,7 +69,8 @@ public class CatalogPagesListComposer extends MessageComposer {
this.response.appendBoolean(category.isVisible());
this.response.appendInt(category.getIconImage());
this.response.appendInt(category.isEnabled() ? category.getId() : -1);
this.response.appendInt(category.isEnabled() || this.hasPermission ? category.getId() : -1);
this.response.appendInt(category.getParentId());
this.response.appendString(category.getPageName());
this.response.appendString(category.getCaption() + (this.hasPermission ? " (" + category.getId() + ")" : ""));