From 6126c35779f4635b1c659b87999753aba4a92849 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:30:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(housekeeping):=20economy=20domain=20?= =?UTF-8?q?=E2=80=94=20credits/currency/items/hc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Incoming 9117 HousekeepingGiveCreditsEvent — Habbo.giveCredits for online (ships UserCreditsComposer) or UPDATE users.credits for offline. * Incoming 9118 HousekeepingGiveCurrencyEvent — generic across the non-credits currencies. currencyType 0 => duckets/pixels (givePixels), 5 => diamonds (givePoints(5,n)), anything else routes through givePoints(type,n). Offline path INSERT ... ON DUPLICATE KEY UPDATE users_currency. * Incoming 9119 HousekeepingGrantItemEvent — batch-INSERT N rows into the items table with item_id = base furni id. Capped at 100 per call so a typo can't bury the DB. Online inventory refresh deferred — the user picks the new items up on next hand-inventory open or relog. * Incoming 9120 HousekeepingSetHcSubscriptionEvent — extends users_settings.club_expire_timestamp by `days*86400`. Stacks on top of the existing expiry if it's still in the future, otherwise starts from now. days==0 clamps to now (effective cancel). All four reuse HousekeepingActionResultComposer (no new outgoing composer this slice). `mvn compile` clean. --- .../com/eu/habbo/messages/PacketManager.java | 4 + .../eu/habbo/messages/incoming/Incoming.java | 4 + .../HousekeepingGiveCreditsEvent.java | 62 +++++++++++++++ .../HousekeepingGiveCurrencyEvent.java | 74 ++++++++++++++++++ .../HousekeepingGrantItemEvent.java | 62 +++++++++++++++ .../HousekeepingSetHcSubscriptionEvent.java | 76 +++++++++++++++++++ 6 files changed, 282 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 564a8a7a..b7ec88f4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -735,5 +735,9 @@ public class PacketManager { this.registerHandler(Incoming.HousekeepingKickAllFromRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickAllFromRoomEvent.class); this.registerHandler(Incoming.HousekeepingTransferRoomOwnershipEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTransferRoomOwnershipEvent.class); this.registerHandler(Incoming.HousekeepingDeleteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingDeleteRoomEvent.class); + this.registerHandler(Incoming.HousekeepingGiveCreditsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCreditsEvent.class); + this.registerHandler(Incoming.HousekeepingGiveCurrencyEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCurrencyEvent.class); + this.registerHandler(Incoming.HousekeepingGrantItemEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGrantItemEvent.class); + this.registerHandler(Incoming.HousekeepingSetHcSubscriptionEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetHcSubscriptionEvent.class); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index aeb4a766..bf9dcfa8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -479,4 +479,8 @@ public class Incoming { public static final int HousekeepingKickAllFromRoomEvent = 9114; public static final int HousekeepingTransferRoomOwnershipEvent = 9115; public static final int HousekeepingDeleteRoomEvent = 9116; + public static final int HousekeepingGiveCreditsEvent = 9117; + public static final int HousekeepingGiveCurrencyEvent = 9118; + public static final int HousekeepingGrantItemEvent = 9119; + public static final int HousekeepingSetHcSubscriptionEvent = 9120; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java new file mode 100644 index 00000000..e0d16e2d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java @@ -0,0 +1,62 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class HousekeepingGiveCreditsEvent extends MessageHandler { + private static final String ACTION_KEY = "user.give_credits"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int amount = this.packet.readInt(); + + if (userId <= 0 || amount == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + // giveCredits already pushes UserCreditsComposer and persists via the + // standard HabboInfo write path; nothing extra needed for the online branch. + online.giveCredits(amount); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1")) { + statement.setInt(1, amount); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java new file mode 100644 index 00000000..ba347b93 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java @@ -0,0 +1,74 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Generic non-credits currency grant. Wire field `currencyType`: + * 0 => duckets / pixels, 5 => diamonds, 101 => seasonal-primary. + * Online users go through Habbo.givePoints / givePixels which dispatches + * a UserCurrencyComposer; offline goes straight to `users_currency`. + */ +public class HousekeepingGiveCurrencyEvent extends MessageHandler { + private static final int CURRENCY_DUCKETS = 0; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int currencyType = this.packet.readInt(); + int amount = this.packet.readInt(); + + String actionKey = "user.give_currency_" + currencyType; + + if (userId <= 0 || amount == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + // givePixels writes users_currency type=0 and ships UserCurrency; + // givePoints(type, amount) is the generalised path for everything else. + if (currencyType == CURRENCY_DUCKETS) { + online.givePixels(amount); + } else { + online.givePoints(currencyType, amount); + } + + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, "")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)")) { + statement.setInt(1, userId); + statement.setInt(2, currencyType); + statement.setInt(3, amount); + statement.executeUpdate(); + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java new file mode 100644 index 00000000..cf1ecb6b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java @@ -0,0 +1,62 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Grant a furni item (by items_base id) `quantity` times. Each row in + * the `items` table is one furni unit; quantity > 1 just batches the + * insert. The online user's HabboInventory isn't proactively refreshed + * — they'll see the new items next time they open the hand inventory + * (or after a relog). + */ +public class HousekeepingGrantItemEvent extends MessageHandler { + private static final String ACTION_KEY = "user.grant_item"; + private static final int MAX_QUANTITY_PER_CALL = 100; + + @Override + public int getRatelimit() { + return 2000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int itemId = this.packet.readInt(); + int quantity = this.packet.readInt(); + + if (userId <= 0 || itemId <= 0 || quantity <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + if (quantity > MAX_QUANTITY_PER_CALL) { + quantity = MAX_QUANTITY_PER_CALL; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data) VALUES (?, ?, '')")) { + for (int i = 0; i < quantity; i++) { + statement.setInt(1, userId); + statement.setInt(2, itemId); + statement.addBatch(); + } + statement.executeBatch(); + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.economy_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java new file mode 100644 index 00000000..9facf4f4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java @@ -0,0 +1,76 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Extend a user's HC by `days`. Adds to the existing club_expire_timestamp + * if it's still in the future, otherwise stretches from `now`. Days==0 + * means cancel the active subscription (timestamp clamped to `now`). + */ +public class HousekeepingSetHcSubscriptionEvent extends MessageHandler { + private static final String ACTION_KEY = "user.set_hc"; + private static final int SECONDS_IN_DAY = 24 * 3600; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int days = this.packet.readInt(); + + if (userId <= 0 || days < 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + int now = Emulator.getIntUnixTimestamp(); + int newExpire; + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (days == 0) { + newExpire = now; + } else if (online != null) { + int current = online.getHabboStats().getClubExpireTimestamp(); + newExpire = (current > now ? current : now) + (days * SECONDS_IN_DAY); + } else { + newExpire = now + (days * SECONDS_IN_DAY); // best-effort offline; can't read previous expiry cheaply + } + + if (online != null) { + online.getHabboStats().setClubExpireTimestamp(newExpire); + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET club_expire_timestamp = ? WHERE user_id = ? LIMIT 1")) { + statement.setInt(1, newExpire); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +}