feat(housekeeping): economy domain — credits/currency/items/hc

* 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.
This commit is contained in:
simoleo89
2026-05-24 11:30:50 +02:00
committed by simoleo89
parent a1749c9eda
commit 6126c35779
6 changed files with 282 additions and 0 deletions
@@ -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);
}
}
@@ -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;
}
@@ -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, ""));
}
}
@@ -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, ""));
}
}
@@ -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, ""));
}
}
@@ -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, ""));
}
}