Merge branch 'dev' into chore/deps-resilience-validation

This commit is contained in:
DuckieTM
2026-06-15 07:24:02 +02:00
committed by GitHub
82 changed files with 2418 additions and 264 deletions
+110 -27
View File
@@ -18,6 +18,8 @@ import com.eu.habbo.plugin.events.emulator.EmulatorStartShutdownEvent;
import com.eu.habbo.plugin.events.emulator.EmulatorStoppedEvent;
import com.eu.habbo.threading.ThreadPooling;
import com.eu.habbo.util.imager.badges.BadgeImager;
import com.eu.habbo.util.logback.ConsoleStyle;
import org.fusesource.jansi.AnsiConsole;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,6 +40,12 @@ public final class Emulator {
private static final Logger LOGGER = LoggerFactory.getLogger(Emulator.class);
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
private static final String ANSI_RESET = "\u001B[0m";
private static final String ANSI_BOLD = "\u001B[1m";
private static final String ANSI_CYAN = "\u001B[36m";
private static final String ANSI_GREEN = "\u001B[32m";
private static final String ANSI_YELLOW = "\u001B[33m";
private static final String ANSI_DIM = "\u001B[2m";
// Fallback version, only used when running outside a packaged jar (e.g. from
// the IDE). In production the version comes from the jar manifest below.
@@ -65,7 +73,6 @@ public final class Emulator {
"██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝███████║ ██║ ██║ ██║██║ ██║\n" +
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝\n" +
"Still Rocking in 2026.\n";
public static String build = "";
public static long buildTimestamp = -1L;
@@ -104,14 +111,12 @@ public final class Emulator {
public static void main(String[] args) throws Exception {
try {
if (OS_NAME.startsWith("Windows") && !CLASS_PATH.contains("idea_rt.jar")) {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console");
appender.stop();
appender.setWithJansi(true);
appender.start();
}
boolean styledConsole = shouldStyleConsole(
System.getenv(),
System.console() != null,
OS_NAME,
System.getProperty("habbo.console.style", "auto"));
configureAnsiConsole(styledConsole);
Locale.setDefault(Locale.of("en"));
setBuild();
@@ -119,7 +124,7 @@ public final class Emulator {
ConsoleCommand.load();
Emulator.logging = new Logging();
System.out.println(logo);
System.out.println(startupHero(styledConsole));
long startTime = System.nanoTime();
@@ -153,23 +158,10 @@ public final class Emulator {
Emulator.config.register("camera.price.points.type", "5");
Emulator.config.register("camera.render.delay", "5");
Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId());
Emulator.config.register("rcon.rate_limit.enabled", "1");
Emulator.config.register("rcon.rate_limit.limit_for_period", "60");
Emulator.config.register("rcon.rate_limit.refresh_period_ms", "1000");
Emulator.config.register("rcon.rate_limit.timeout_ms", "0");
Emulator.config.register("rcon.mute.max_duration_seconds", "604800");
Emulator.config.register("rcon.achievement.max_progress", "10000");
Emulator.config.register("rcon.execute_command.max_length", "256");
Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_update_config;cmd_update_permissions;cmd_give_rank;cmd_badge;cmd_gift;cmd_credits;cmd_points;cmd_pixels;cmd_massbadge;cmd_masscredits;cmd_massgift;cmd_massduckets;cmd_masspoints;cmd_empty;cmd_empty_bots;cmd_empty_pets;cmd_unload;cmd_ban;cmd_superban;cmd_ip_ban;cmd_machine_ban;cmd_disconnect");
Emulator.config.register("rcon.execute_command.allowed_permissions", "");
Emulator.config.register("gui.enabled", "0");
Emulator.config.register("gui.autostart.enabled", "0");
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
System.out.println();
LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, ");
System.out.println();
LOGGER.info("This project is for educational purposes only. This Emulator is an open-source fork of Arcturus created by TheGeneral.");
LOGGER.info("Version: {}", version);
LOGGER.info("Build: {}", build);
LOGGER.info("Build Timestamp: {} [{}]", formatBuildTimestamp(buildTimestamp, hotelTimezoneId), hotelTimezoneId);
System.out.println(startupCard(hotelTimezoneId));
Emulator.texts.register("camera.permission", "You don't have permission to use the camera!");
Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture.");
Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*");
@@ -207,7 +199,7 @@ public final class Emulator {
Emulator.isReady = true;
Emulator.timeStarted = getIntUnixTimestamp();
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
if (shouldLaunchGui()) {
EmulatorDashboard.launch();
}
@@ -319,6 +311,97 @@ public final class Emulator {
return -1L;
}
static String startupCard(String hotelTimezoneId) {
return "\n" +
"+----------------------------------------------------------------+\n" +
"| Arcturus Morningstar Extended |\n" +
"| Source : github.com/duckietm/Arcturus-Morningstar-Extended |\n" +
"| Scope : Educational open-source fork by TheGeneral |\n" +
"| Version: " + version + "\n" +
"| Build : " + build + "\n" +
"| Time : " + formatBuildTimestamp(buildTimestamp, hotelTimezoneId) + " [" + hotelTimezoneId + "]\n" +
"+----------------------------------------------------------------+\n";
}
static String startupHero() {
return startupHero(false);
}
static String startupHero(boolean styled) {
if (styled) {
return "\n" +
ANSI_CYAN +
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
ANSI_RESET +
"\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
"| " + ANSI_BOLD + ANSI_GREEN + "[OK] MORNINGSTAR EXTENDED" + ANSI_RESET + fit("", 50) + " |\n" +
"| " + ANSI_DIM + "Arcturus game server runtime" + ANSI_RESET + fit("", 48) + " |\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
"| " + ANSI_YELLOW + "[VER]" + ANSI_RESET + " Version : " + fit(version, 57) + " |\n" +
"| " + ANSI_YELLOW + "[BLD]" + ANSI_RESET + " Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 57) + " |\n" +
"| " + ANSI_YELLOW + "[JVM]" + ANSI_RESET + " Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / styled console output", 57) + " |\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n";
}
return "\n" +
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
"\n" +
"+------------------------------------------------------------------------------+\n" +
"| MORNINGSTAR EXTENDED |\n" +
"| Arcturus game server runtime |\n" +
"+------------------------------------------------------------------------------+\n" +
"| Version : " + fit(version, 63) + " |\n" +
"| Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 63) + " |\n" +
"| Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / universal console output", 63) + " |\n" +
"+------------------------------------------------------------------------------+\n";
}
static boolean shouldStyleConsole(Map<String, String> environment, boolean interactiveConsole, String osName, String styleProperty) {
return ConsoleStyle.isEnabled(environment, interactiveConsole, osName, styleProperty);
}
static void configureAnsiConsole(boolean styledConsole) {
if (!styledConsole || !OS_NAME.startsWith("Windows") || CLASS_PATH.contains("idea_rt.jar")) {
return;
}
try {
AnsiConsole.systemInstall();
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console");
if (appender != null) {
appender.stop();
appender.setWithJansi(true);
appender.start();
}
} catch (Throwable e) {
LOGGER.debug("Unable to install Jansi console bridge; continuing with raw console output.", e);
}
}
static boolean shouldLaunchGui() {
return Emulator.getConfig() != null && Emulator.getConfig().getBoolean("gui.autostart.enabled", false);
}
private static String fit(String value, int width) {
String safe = value == null ? "" : value;
if (safe.length() > width) {
return safe.substring(0, Math.max(0, width - 3)) + "...";
}
return String.format("%-" + width + "s", safe);
}
private static String formatBuildTimestamp(long buildTimestamp, String timezoneId) {
if (buildTimestamp <= 0) {
return "UNKNOWN";
@@ -52,6 +52,10 @@ public class TextsManager {
return this.texts.getProperty(key, defaultValue);
}
public String getValueQuietly(String key, String defaultValue) {
return this.texts.getProperty(key, defaultValue);
}
public boolean getBoolean(String key) {
return this.getBoolean(key, false);
}
@@ -179,9 +179,13 @@ public class BotManager {
}
public void pickUpBot(Bot bot, Habbo habbo) {
HabboInfo receiverInfo = habbo == null ? Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId()) : habbo.getHabboInfo();
if (bot != null) {
HabboInfo receiverInfo = resolvePickupReceiver(bot, habbo);
Room botRoom = bot.getRoom();
if (receiverInfo == null || botRoom == null) {
return;
}
BotPickUpEvent pickedUpEvent = new BotPickUpEvent(bot, habbo);
Emulator.getPluginManager().fireEvent(pickedUpEvent);
@@ -198,8 +202,8 @@ public class BotManager {
return;
}
bot.onPickUp(habbo, receiverInfo.getCurrentRoom());
receiverInfo.getCurrentRoom().removeBot(bot);
bot.onPickUp(habbo, botRoom);
botRoom.removeBot(bot);
bot.stopFollowingHabbo();
bot.setOwnerId(receiverInfo.getId());
bot.setOwnerName(receiverInfo.getUsername());
@@ -215,6 +219,14 @@ public class BotManager {
}
}
private HabboInfo resolvePickupReceiver(Bot bot, Habbo picker) {
if (picker != null && bot.getOwnerId() == picker.getHabboInfo().getId()) {
return picker.getHabboInfo();
}
return Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId());
}
public Bot loadBot(ResultSet set) {
try {
String type = set.getString("type");
@@ -711,18 +711,22 @@ public class CatalogManager {
return;
}
if (voucher.isExhausted()) {
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR));
return;
Voucher.ClaimResult claimResult = voucher.claimForUser(habbo.getHabboInfo().getId());
switch (claimResult) {
case CLAIMED:
break;
case EXHAUSTED:
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR));
return;
case USER_LIMIT:
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
return;
case FAILED:
default:
client.sendResponse(new RedeemVoucherErrorComposer(RedeemVoucherErrorComposer.TECHNICAL_ERROR));
return;
}
if (voucher.hasUserExhausted(habbo.getHabboInfo().getId())) {
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
return;
}
voucher.addHistoryEntry(habbo.getHabboInfo().getId());
if (voucher.points > 0) {
client.getHabbo().givePoints(voucher.pointsType, voucher.points);
}
@@ -14,6 +14,13 @@ import java.util.List;
public class Voucher {
private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class);
public enum ClaimResult {
CLAIMED,
EXHAUSTED,
USER_LIMIT,
FAILED
}
public final int id;
public final String code;
public final int credits;
@@ -58,18 +65,34 @@ public class Voucher {
return this.amount > 0 && this.history.size() >= this.amount;
}
public void addHistoryEntry(int userId) {
int timestamp = Emulator.getIntUnixTimestamp();
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp));
public synchronized ClaimResult claimForUser(int userId) {
if (this.isExhausted()) {
return ClaimResult.EXHAUSTED;
}
if (this.hasUserExhausted(userId)) {
return ClaimResult.USER_LIMIT;
}
int timestamp = Emulator.getIntUnixTimestamp();
if (!this.insertHistoryEntry(userId, timestamp)) {
return ClaimResult.FAILED;
}
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp));
return ClaimResult.CLAIMED;
}
private boolean insertHistoryEntry(int userId, int timestamp) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) {
statement.setInt(1, this.id);
statement.setInt(2, userId);
statement.setInt(3, timestamp);
statement.execute();
return statement.executeUpdate() > 0;
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
return false;
}
}
}
@@ -398,11 +398,12 @@ public class MarketPlace {
synchronized (client.getHabbo().getInventory()) {
for (MarketPlaceOffer offer : offers) {
if (offer.getState().equals(MarketPlaceState.SOLD)) {
client.getHabbo().getInventory().removeMarketplaceOffer(offer);
credits += offer.getPrice();
removeUser(offer);
offer.needsUpdate(true);
Emulator.getThreading().run(offer);
if (removeUser(offer)) {
client.getHabbo().getInventory().removeMarketplaceOffer(offer);
credits += offer.getPrice();
offer.needsUpdate(true);
Emulator.getThreading().run(offer);
}
}
}
}
@@ -416,13 +417,14 @@ public class MarketPlace {
}
}
private static void removeUser(MarketPlaceOffer offer) {
private static boolean removeUser(MarketPlaceOffer offer) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE marketplace_items SET user_id = ? WHERE id = ?")) {
statement.setInt(1, -1);
statement.setInt(2, offer.getOfferId());
statement.execute();
return statement.executeUpdate() > 0;
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
return false;
}
}
@@ -18,7 +18,7 @@ public class CommandsCommand extends Command {
for (Command c : commands) {
String textKey = "commands.description." + c.permission;
String commandText = Emulator.getTexts().getValue(textKey, "");
String commandText = Emulator.getTexts().getValueQuietly(textKey, "");
String commandLine = ":" + c.keys[0];
String description = "";
@@ -0,0 +1,52 @@
package com.eu.habbo.habbohotel.guilds;
import com.eu.habbo.messages.ClientMessage;
import com.eu.habbo.util.PacketGuard;
public final class GuildBadgeBuilder {
public static final int MAX_BADGE_PARTS = 5;
private static final int INTS_PER_PART = 3;
private static final int BYTES_PER_INT = 4;
private static final int MAX_PART_ID = 999;
private static final int MAX_COLOR_ID = 99;
private static final int MAX_POSITION = 8;
private GuildBadgeBuilder() {
}
public static String readBadge(ClientMessage packet, int flatPartValueCount) {
if (flatPartValueCount % INTS_PER_PART != 0) {
return null;
}
int partCount = flatPartValueCount / INTS_PER_PART;
if (!PacketGuard.isCountInRange(partCount, 1, MAX_BADGE_PARTS)
|| !PacketGuard.hasFixedWidthEntries(packet, flatPartValueCount, BYTES_PER_INT)) {
return null;
}
StringBuilder badge = new StringBuilder(partCount * 6);
for (int partIndex = 0; partIndex < partCount; partIndex++) {
int id = packet.readInt();
int color = packet.readInt();
int position = packet.readInt();
if (!isValidPart(id, color, position)) {
return null;
}
badge.append(partIndex == 0 ? "b" : "s");
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id);
badge.append(color < 10 ? "0" : "").append(color);
badge.append(position);
}
return badge.toString();
}
private static boolean isValidPart(int id, int color, int position) {
return id >= 0 && id <= MAX_PART_ID
&& color >= 0 && color <= MAX_COLOR_ID
&& position >= 0 && position <= MAX_POSITION;
}
}
@@ -291,11 +291,12 @@ public class GuildManager {
}
}
} else if (!error) {
try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ?")) {
try (PreparedStatement statement = connection.prepareStatement("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ? AND level_id = ?")) {
statement.setInt(1, GuildRank.MEMBER.type);
statement.setInt(2, Emulator.getIntUnixTimestamp());
statement.setInt(3, userId);
statement.setInt(4, guild.getId());
statement.setInt(5, GuildRank.REQUESTED.type);
statement.execute();
}
}
@@ -0,0 +1,52 @@
package com.eu.habbo.habbohotel.items;
/**
* Builds a complete furnidata entry object (single-line JSON5) from an {@link Item}
* (its items_base row) plus a display name/description. Used by the Furni Editor
* upsert path when a furni has no furnidata entry yet. Field shape mirrors the
* hotel's existing furnidata entries; {@code id} is the item's sprite id so the
* renderer resolves the furni's name/data by typeId.
*/
public final class FurnidataEntryBuilder {
private FurnidataEntryBuilder() {}
public static String build(Item item, String name, String description) {
String classname = item.getName() != null ? item.getName() : "";
String safeName = (name != null && !name.isBlank()) ? name
: (item.getFullName() != null && !item.getFullName().isBlank()) ? item.getFullName()
: classname;
String safeDesc = description != null ? description : "";
String customParams = item.getCustomParams() != null ? item.getCustomParams() : "";
StringBuilder b = new StringBuilder(256);
b.append("{\"id\":").append(item.getSpriteId());
b.append(",\"classname\":\"").append(esc(classname)).append('"');
b.append(",\"revision\":0,\"category\":\"unknown\",\"defaultdir\":0");
b.append(",\"xdim\":").append(item.getWidth());
b.append(",\"ydim\":").append(item.getLength());
b.append(",\"partcolors\":{\"color\":[]}");
b.append(",\"name\":\"").append(esc(safeName)).append('"');
b.append(",\"description\":\"").append(esc(safeDesc)).append('"');
b.append(",\"adurl\":\"\",\"offerid\":-1,\"buyout\":false,\"rentofferid\":-1,\"rentbuyout\":false,\"bc\":false,\"excludeddynamic\":false");
b.append(",\"customparams\":\"").append(esc(customParams)).append('"');
b.append(",\"specialtype\":1");
b.append(",\"canstandon\":").append(item.allowWalk());
b.append(",\"cansiton\":").append(item.allowSit());
b.append(",\"canlayon\":").append(item.allowLay());
b.append('}');
return b.toString();
}
/** Escape for a JSON string value; collapse control chars to spaces. */
private static String esc(String v) {
StringBuilder b = new StringBuilder(v.length() + 8);
for (int i = 0; i < v.length(); i++) {
char c = v.charAt(i);
if (c == '"' || c == '\\') b.append('\\').append(c);
else if (c == '\n' || c == '\r' || c == '\t') b.append(' ');
else b.append(c);
}
return b.toString();
}
}
@@ -36,30 +36,36 @@ public final class FurnidataSourceResolver {
public static Source resolve() {
try {
String override = Emulator.getConfig().getValue("items.furnidata.path", "");
if (!override.isEmpty()) {
Path p = Paths.get(override);
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path");
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path does not exist");
}
String rendererConfigPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
String assetBasePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
if (!rendererConfigPath.isEmpty()) {
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
}
Source fallback = resolveFromAssetBase(assetBasePath);
if (fallback != null) return fallback;
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
return resolveConfigured(override, rendererConfigPath, assetBasePath);
} catch (Exception e) {
LOGGER.warn("FurnidataSourceResolver failed", e);
return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "Resolver error");
}
}
public static Source resolveConfigured(String legacyOverridePath, String rendererConfigPath, String assetBasePath) {
if (rendererConfigPath != null && !rendererConfigPath.isEmpty()) {
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath == null || assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
}
Source fromAssetBase = resolveFromAssetBase(assetBasePath);
if (fromAssetBase != null && fromAssetBase.ok()) return fromAssetBase;
if (legacyOverridePath != null && !legacyOverridePath.isEmpty()) {
Path p = Paths.get(legacyOverridePath);
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path fallback");
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path fallback does not exist");
}
if (fromAssetBase != null) return fromAssetBase;
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
}
public static Source resolveFromRendererConfig(Path rendererConfig, Path assetBase) {
try {
if (rendererConfig == null || !Files.exists(rendererConfig)) {
@@ -56,6 +56,98 @@ public class FurnidataWriter {
return true;
}
/** Outcome of a {@link #create} attempt. */
public enum CreateResult { CREATED, ALREADY_EXISTS, ID_COLLISION, NO_TARGET, IO_ERROR }
/**
* Append a brand-new furnidata entry (upsert's "create" half). Refuses if the
* classname already exists (caller should edit instead) or if {@code id} is
* already used by a DIFFERENT classname (id collision would break the
* {@code roomItem.name.<id>} / typeId resolution on the renderer). The complete
* entry object is built by the caller (see FurnidataEntryBuilder) and inserted
* right after the opening '[' of the matching section's "furnitype" array.
*
* @param classname new classname (must be absent from furnidata)
* @param id furnidata id (= item sprite id); must not collide
* @param type FLOOR -> roomitemtypes, WALL -> wallitemtypes
* @param entryJson5 the complete entry object as a single-line JSON5 string
* @param createTier split-tier only: the tier dir to write into (e.g. "custom"); ignored for single-file
*/
public CreateResult create(String classname, int id, FurnitureType type, String entryJson5, String createTier) {
String cn = classname == null ? "" : classname.trim().toLowerCase(java.util.Locale.ROOT);
if (cn.isEmpty() || entryJson5 == null || entryJson5.isBlank()) return CreateResult.NO_TARGET;
// Guard: duplicate classname / id collision (scan the whole source).
for (FurnidataEntry e : new FurnidataReader(source, maxBytes).read()) {
String ecn = e.classname() == null ? "" : e.classname().trim().toLowerCase(java.util.Locale.ROOT);
if (ecn.equals(cn)) return CreateResult.ALREADY_EXISTS;
if (e.id() == id) return CreateResult.ID_COLLISION;
}
try {
Path target = resolveCreateTarget(createTier);
if (target == null) return CreateResult.NO_TARGET;
String raw = Files.readString(target, StandardCharsets.UTF_8);
String section = (type == FurnitureType.WALL) ? "wallitemtypes" : "roomitemtypes";
int open = furnitypeArrayOpenIndex(raw, section);
if (open < 0) return CreateResult.NO_TARGET; // section/array absent in target file
String edited = raw.substring(0, open) + "\n" + entryJson5 + "," + raw.substring(open);
backup(target);
atomicWrite(target, edited);
return CreateResult.CREATED;
} catch (IOException e) {
return CreateResult.IO_ERROR;
}
}
/** Single-file: the source. Split-tier: the create-tier file (created with a shell if absent). */
private Path resolveCreateTarget(String createTier) throws IOException {
if (!directory) return source;
String tier = (createTier == null || createTier.isBlank()) ? "custom" : createTier.trim();
Path base = source.toAbsolutePath().normalize();
Path tierDir = safeResolve(base, tier);
if (tierDir == null) return null;
if (!Files.isDirectory(tierDir)) Files.createDirectories(tierDir);
for (String fileName : manifestList(tierDir, "files", List.of())) {
Path f = safeResolve(base, tierDir.resolve(fileName).toString());
if (f != null && Files.isRegularFile(f)) return f;
}
Path def = tierDir.resolve("furnidata.json5");
if (!Files.exists(def)) {
Files.writeString(def,
"{\n \"roomitemtypes\": { \"furnitype\": [\n] },\n \"wallitemtypes\": { \"furnitype\": [\n] }\n}\n",
StandardCharsets.UTF_8);
}
return def;
}
/** Index just after the '[' that opens {@code <section>.furnitype}, or -1 if absent. String-aware. */
static int furnitypeArrayOpenIndex(String raw, String section) {
int s = indexOfKey(raw, section, 0);
if (s < 0) return -1;
int ft = indexOfKey(raw, "furnitype", s);
if (ft < 0) return -1;
boolean inStr = false; char q = 0;
for (int i = ft; i < raw.length(); i++) {
char c = raw.charAt(i);
if (inStr) { if (c == '\\') i++; else if (c == q) inStr = false; continue; }
if (c == '"' || c == '\'') { inStr = true; q = c; }
else if (c == '[') return i + 1;
}
return -1;
}
/** First occurrence of a quoted key ("key" or 'key') at/after {@code from}, or -1. */
private static int indexOfKey(String raw, String key, int from) {
int a = raw.indexOf("\"" + key + "\"", from);
int b = raw.indexOf("'" + key + "'", from);
if (a < 0) return b;
if (b < 0) return a;
return Math.min(a, b);
}
/** For single-file just returns the file; for split-tier, the tier file that contains cn. */
private Path locateFile(String cn) throws IOException {
if (!directory) {
@@ -27,6 +27,7 @@ public class FurnitureTextProvider {
private final boolean enabled;
private volatile Map<String, FurniText> index = Map.of();
private volatile Path source;
private volatile String sourceDescription = "unknown";
private FurnidataWatcher watcher;
public FurnitureTextProvider(boolean enabled) {
@@ -47,7 +48,7 @@ public class FurnitureTextProvider {
return;
}
reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read());
LOGGER.info("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source);
LOGGER.info("Furniture Text Provider -> Indexed! ({} names, source: {})", this.index.size(), this.sourceDescription);
if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) {
if (this.watcher != null) this.watcher.stop();
@@ -88,9 +89,12 @@ public class FurnitureTextProvider {
}
}
private static Path resolveSource() {
private Path resolveSource() {
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve();
if (source.ok()) return source.path();
if (source.ok()) {
this.sourceDescription = source.message();
return source.path();
}
LOGGER.warn("FurnitureTextProvider: no furnidata source resolved ({}) - {}", source.status(), source.message());
return null;
}
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.items.interactions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomLayout;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
@@ -133,12 +134,18 @@ public class InteractionRentableSpace extends HabboItem {
if (habbo.getHabboStats().isRentingSpace())
return;
if (habbo.getHabboInfo().getCredits() < this.rentCost())
int cost = this.rentCost();
boolean hasInfiniteCredits = habbo.hasPermission(Permission.ACC_INFINITE_CREDITS);
if (!hasInfiniteCredits && habbo.getHabboInfo().getCredits() < cost)
return;
if (habbo.getHabboStats().getClubExpireTimestamp() < Emulator.getIntUnixTimestamp())
return;
if (!hasInfiniteCredits) {
habbo.giveCredits(-cost);
}
this.setRenterId(habbo.getHabboInfo().getId());
this.setRenterName(habbo.getHabboInfo().getUsername());
this.setEndTimestamp(Emulator.getIntUnixTimestamp() + (7 * 86400));
@@ -58,7 +58,7 @@ public class RoomTrade {
public synchronized void offerItem(Habbo habbo, HabboItem item) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
if (user.getItems().contains(item))
if (user == null || item == null || user.getItems().contains(item))
return;
habbo.getInventory().getItemsComponent().removeHabboItem(item);
@@ -71,6 +71,9 @@ public class RoomTrade {
public synchronized void offerMultipleItems(Habbo habbo, THashSet<HabboItem> items) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
if (user == null || items == null)
return;
for (HabboItem item : items) {
if (!user.getItems().contains(item)) {
habbo.getInventory().getItemsComponent().removeHabboItem(item);
@@ -85,7 +88,7 @@ public class RoomTrade {
public synchronized void removeItem(Habbo habbo, HabboItem item) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
if (!user.getItems().contains(item))
if (user == null || item == null || !user.getItems().contains(item))
return;
habbo.getInventory().getItemsComponent().addItem(item);
@@ -98,6 +101,9 @@ public class RoomTrade {
public synchronized void accept(Habbo habbo, boolean value) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
if (user == null)
return;
user.setAccepted(value);
this.sendMessageToUsers(new TradeAcceptedComposer(user));
@@ -120,6 +126,9 @@ public class RoomTrade {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
if (user == null)
return;
user.confirm();
this.sendMessageToUsers(new TradeAcceptedComposer(user));
@@ -134,6 +143,12 @@ public class RoomTrade {
if (this.tradeItems()) {
this.closeWindow();
this.sendMessageToUsers(new TradeCompleteComposer());
} else {
this.returnItems();
for (RoomTradeUser roomTradeUser : this.users) {
roomTradeUser.clearItems();
}
this.closeWindow();
}
this.room.stopTrade(this);
@@ -188,7 +203,6 @@ public class RoomTrade {
try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET user_id = ? WHERE id = ? LIMIT 1")) {
try (PreparedStatement stmt = connection.prepareStatement("INSERT INTO room_trade_log_items (id, item_id, user_id) VALUES (?, ?, ?)")) {
for (HabboItem item : userOne.getItems()) {
item.setUserId(userTwoId);
statement.setInt(1, userTwoId);
statement.setInt(2, item.getId());
statement.addBatch();
@@ -202,7 +216,6 @@ public class RoomTrade {
}
for (HabboItem item : userTwo.getItems()) {
item.setUserId(userOneId);
statement.setInt(1, userOneId);
statement.setInt(2, item.getId());
statement.addBatch();
@@ -224,6 +237,16 @@ public class RoomTrade {
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
this.sendMessageToUsers(new TradeClosedComposer(userOne.getHabbo().getRoomUnit().getId(), TradeClosedComposer.ITEMS_NOT_FOUND));
return false;
}
for (HabboItem item : userOne.getItems()) {
item.setUserId(userTwo.getHabbo().getHabboInfo().getId());
}
for (HabboItem item : userTwo.getItems()) {
item.setUserId(userOne.getHabbo().getHabboInfo().getId());
}
THashSet<HabboItem> itemsUserOne = new THashSet<>(userOne.getItems());
@@ -19,8 +19,13 @@ public class RoomTradeManager {
* Starts a trade between two users.
*/
public void startTrade(Habbo userOne, Habbo userTwo) {
RoomTrade trade = new RoomTrade(userOne, userTwo, this.room);
RoomTrade trade;
synchronized (this.activeTrades) {
if (this.hasActiveTrade(userOne) || this.hasActiveTrade(userTwo)) {
return;
}
trade = new RoomTrade(userOne, userTwo, this.room);
this.activeTrades.add(trade);
}
@@ -58,4 +63,16 @@ public class RoomTradeManager {
public THashSet<RoomTrade> getActiveTrades() {
return this.activeTrades;
}
private boolean hasActiveTrade(Habbo user) {
for (RoomTrade trade : this.activeTrades) {
for (RoomTradeUser habbo : trade.getRoomTradeUsers()) {
if (habbo.getHabbo() == user) {
return true;
}
}
}
return false;
}
}
@@ -90,7 +90,13 @@ public class InfostandBackgroundManager {
this.enforce = loaded > 0;
if (this.enforce) {
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards, {} borders from infostand_backgrounds.",
LOGGER.info(summary(
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
this.entries.get(Category.CARD).size(),
this.entries.get(Category.BORDER).size()));
LOGGER.debug("Infostand Background Manager assets: {} bg, {} stands, {} overlays, {} cards, {} borders",
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
@@ -101,6 +107,11 @@ public class InfostandBackgroundManager {
}
}
static String summary(int backgrounds, int stands, int overlays, int cards, int borders) {
int total = backgrounds + stands + overlays + cards + borders;
return String.format("Infostand Background Manager -> Loaded! (%d assets)", total);
}
public boolean canUse(Habbo habbo, Category category, int id) {
if (id == 0) return true;
if (!this.enforce) return true;
@@ -83,7 +83,7 @@ public class Incoming {
public static final int GuildAcceptMembershipEvent = 3386;
public static final int RequestRecylerLogicEvent = 398;
public static final int RequestGuildJoinEvent = 998;
public static final int RequestCatalogIndexEvent = 2529;
public static int RequestCatalogIndexEvent = 2529;
public static final int BuildersClubQueryFurniCountEvent = 2529;
public static final int BuildersClubPlaceRoomItemEvent = 1051;
public static final int BuildersClubPlaceWallItemEvent = 462;
@@ -35,33 +35,45 @@ public class CatalogAdminCreateOfferEvent extends MessageHandler {
int orderNumber = this.packet.readInt();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
CatalogAdminOfferPayload payload = CatalogAdminOfferPayload.validate(pageId, itemIds, catalogName, costCredits,
costPoints, pointsType, amount, clubOnly, extradata, haveOffer, offerIdGroup, limitedStack,
orderNumber, pageType);
if (payload == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid offer payload"));
return;
}
if (Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(payload.pageId, payload.pageType) == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + payload.pageId));
return;
}
int newId = -1;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
(pageType == CatalogPageType.BUILDER)
(payload.pageType == CatalogPageType.BUILDER)
? "INSERT INTO catalog_items_bc (page_id, item_ids, catalog_name, order_number, extradata) VALUES (?, ?, ?, ?, ?)"
: "INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, club_only, extradata, have_offer, offer_id, limited_stack, order_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
String cleanItemIds = (itemIds == null || itemIds.trim().isEmpty()) ? "0" : itemIds.trim();
statement.setInt(1, pageId);
statement.setString(2, cleanItemIds);
statement.setString(3, catalogName);
statement.setInt(1, payload.pageId);
statement.setString(2, payload.itemIds);
statement.setString(3, payload.catalogName);
if (pageType == CatalogPageType.BUILDER) {
statement.setInt(4, orderNumber);
statement.setString(5, extradata);
if (payload.pageType == CatalogPageType.BUILDER) {
statement.setInt(4, payload.orderNumber);
statement.setString(5, payload.extradata);
} else {
statement.setInt(4, costCredits);
statement.setInt(5, costPoints);
statement.setInt(6, pointsType);
statement.setInt(7, amount);
statement.setString(8, clubOnly == 1 ? "1" : "0");
statement.setString(9, extradata);
statement.setString(10, haveOffer ? "1" : "0");
statement.setInt(11, offerIdGroup);
statement.setInt(12, limitedStack);
statement.setInt(13, orderNumber);
statement.setInt(4, payload.costCredits);
statement.setInt(5, payload.costPoints);
statement.setInt(6, payload.pointsType);
statement.setInt(7, payload.amount);
statement.setString(8, payload.clubOnly == 1 ? "1" : "0");
statement.setString(9, payload.extradata);
statement.setString(10, payload.haveOffer ? "1" : "0");
statement.setInt(11, payload.offerIdGroup);
statement.setInt(12, payload.limitedStack);
statement.setInt(13, payload.orderNumber);
}
statement.execute();
@@ -36,7 +36,7 @@ public class CatalogAdminCreatePageEvent extends MessageHandler {
pageLayout = CatalogPageLayouts.default_3x3;
}
if (parentId != -1 && Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId) == null) {
if (parentId != -1 && Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId, pageType) == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
return;
}
@@ -36,7 +36,10 @@ public class CatalogAdminDeletePageEvent extends MessageHandler {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
statement.setInt(1, pageId);
statement.execute();
if (statement.executeUpdate() == 0) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
}
Emulator.getGameEnvironment().getCatalogManager().getCatalogPagesMap(pageType).remove(pageId);
@@ -28,12 +28,21 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages";
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
if (page == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
if (newParentId == -1) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) {
statement.setInt(1, pageId);
statement.execute();
if (statement.executeUpdate() == 0) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page toggled"));
return;
@@ -44,30 +53,27 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) {
statement.setInt(1, pageId);
statement.execute();
if (statement.executeUpdate() == 0) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Visibility toggled"));
return;
}
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);
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(newParentId, pageType);
if (parent == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + newParentId));
return;
}
if (this.wouldCreateCycle(pageId, newParentId)) {
if (this.wouldCreateCycle(pageId, newParentId, pageType)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to move: that would create a cycle"));
return;
}
@@ -80,18 +86,21 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
statement.setInt(1, newParentId);
statement.setInt(2, newIndex);
statement.setInt(3, pageId);
statement.execute();
if (statement.executeUpdate() == 0) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page moved"));
}
private boolean wouldCreateCycle(int pageId, int parentId) {
private boolean wouldCreateCycle(int pageId, int parentId, CatalogPageType pageType) {
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);
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current, pageType);
if (parent == null) return false;
current = parent.getParentId();
}
@@ -0,0 +1,125 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
final class CatalogAdminOfferPayload {
private static final int MAX_ITEM_IDS_LENGTH = 512;
private static final int MAX_ITEM_IDS = 100;
private static final int MAX_CATALOG_NAME_LENGTH = 128;
private static final int MAX_EXTRADATA_LENGTH = 1024;
private static final int MAX_CURRENCY_VALUE = 1_000_000_000;
private static final int MAX_AMOUNT = 10_000;
private static final int MAX_POINTS_TYPE = 10_000;
private static final int MAX_ORDER_NUMBER = 1_000_000;
private static final int MAX_LIMITED_STACK = 1_000_000;
final int pageId;
final String itemIds;
final String catalogName;
final int costCredits;
final int costPoints;
final int pointsType;
final int amount;
final int clubOnly;
final String extradata;
final boolean haveOffer;
final int offerIdGroup;
final int limitedStack;
final int orderNumber;
final CatalogPageType pageType;
private CatalogAdminOfferPayload(int pageId, String itemIds, String catalogName, int costCredits, int costPoints,
int pointsType, int amount, int clubOnly, String extradata, boolean haveOffer,
int offerIdGroup, int limitedStack, int orderNumber, CatalogPageType pageType) {
this.pageId = pageId;
this.itemIds = itemIds;
this.catalogName = catalogName;
this.costCredits = costCredits;
this.costPoints = costPoints;
this.pointsType = pointsType;
this.amount = amount;
this.clubOnly = clubOnly;
this.extradata = extradata;
this.haveOffer = haveOffer;
this.offerIdGroup = offerIdGroup;
this.limitedStack = limitedStack;
this.orderNumber = orderNumber;
this.pageType = pageType;
}
static CatalogAdminOfferPayload validate(int pageId, String itemIds, String catalogName, int costCredits,
int costPoints, int pointsType, int amount, int clubOnly,
String extradata, boolean haveOffer, int offerIdGroup,
int limitedStack, int orderNumber, CatalogPageType pageType) {
String cleanItemIds = normalizeItemIds(itemIds);
String cleanCatalogName = clamp(catalogName, MAX_CATALOG_NAME_LENGTH);
String cleanExtradata = clamp(extradata, MAX_EXTRADATA_LENGTH);
if (pageId <= 0
|| cleanItemIds == null
|| cleanCatalogName.isBlank()
|| !isInRange(orderNumber, 0, MAX_ORDER_NUMBER)) {
return null;
}
if (pageType != CatalogPageType.BUILDER) {
if (!isInRange(costCredits, 0, MAX_CURRENCY_VALUE)
|| !isInRange(costPoints, 0, MAX_CURRENCY_VALUE)
|| !isInRange(pointsType, 0, MAX_POINTS_TYPE)
|| !isInRange(amount, 1, MAX_AMOUNT)
|| !isInRange(clubOnly, 0, 1)
|| offerIdGroup < 0
|| !isInRange(limitedStack, 0, MAX_LIMITED_STACK)) {
return null;
}
}
return new CatalogAdminOfferPayload(pageId, cleanItemIds, cleanCatalogName, costCredits, costPoints,
pointsType, amount, clubOnly, cleanExtradata, haveOffer, offerIdGroup, limitedStack, orderNumber,
pageType);
}
private static String normalizeItemIds(String value) {
if (value == null || value.trim().isEmpty()) {
return "0";
}
String clean = value.trim();
if (clean.length() > MAX_ITEM_IDS_LENGTH) {
return null;
}
String[] parts = clean.split(",");
if (parts.length == 0 || parts.length > MAX_ITEM_IDS) {
return null;
}
for (String part : parts) {
if (part.isBlank()) {
return null;
}
try {
if (Integer.parseInt(part.trim()) < 0) {
return null;
}
} catch (NumberFormatException e) {
return null;
}
}
return clean.replaceAll("\\s+", "");
}
private static boolean isInRange(int value, int min, int max) {
return value >= min && value <= max;
}
private static String clamp(String value, int maxLength) {
if (value == null) {
return "";
}
return value.length() <= maxLength ? value : value.substring(0, maxLength);
}
}
@@ -34,10 +34,28 @@ public class CatalogAdminSaveOfferEvent extends MessageHandler {
int orderNumber = this.packet.readInt();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
if (offerId <= 0) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid offer id"));
return;
}
CatalogAdminOfferPayload payload = CatalogAdminOfferPayload.validate(pageId, itemIds, catalogName, costCredits,
costPoints, pointsType, amount, clubOnly, extradata, haveOffer, offerIdGroup, limitedStack,
orderNumber, pageType);
if (payload == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid offer payload"));
return;
}
if (Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(payload.pageId, payload.pageType) == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + payload.pageId));
return;
}
boolean updateItemIds = itemIds != null && !itemIds.trim().isEmpty();
String sql;
if (pageType == CatalogPageType.BUILDER) {
if (payload.pageType == CatalogPageType.BUILDER) {
sql = updateItemIds
? "UPDATE catalog_items_bc SET page_id = ?, item_ids = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?"
: "UPDATE catalog_items_bc SET page_id = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?";
@@ -50,30 +68,33 @@ public class CatalogAdminSaveOfferEvent extends MessageHandler {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
int idx = 1;
statement.setInt(idx++, pageId);
statement.setInt(idx++, payload.pageId);
if (updateItemIds) {
statement.setString(idx++, itemIds.trim());
statement.setString(idx++, payload.itemIds);
}
statement.setString(idx++, catalogName);
statement.setString(idx++, payload.catalogName);
if (pageType == CatalogPageType.BUILDER) {
statement.setInt(idx++, orderNumber);
statement.setString(idx++, extradata);
if (payload.pageType == CatalogPageType.BUILDER) {
statement.setInt(idx++, payload.orderNumber);
statement.setString(idx++, payload.extradata);
statement.setInt(idx, offerId);
} else {
statement.setInt(idx++, costCredits);
statement.setInt(idx++, costPoints);
statement.setInt(idx++, pointsType);
statement.setInt(idx++, amount);
statement.setString(idx++, clubOnly == 1 ? "1" : "0");
statement.setString(idx++, extradata);
statement.setString(idx++, haveOffer ? "1" : "0");
statement.setInt(idx++, offerIdGroup);
statement.setInt(idx++, limitedStack);
statement.setInt(idx++, orderNumber);
statement.setInt(idx++, payload.costCredits);
statement.setInt(idx++, payload.costPoints);
statement.setInt(idx++, payload.pointsType);
statement.setInt(idx++, payload.amount);
statement.setString(idx++, payload.clubOnly == 1 ? "1" : "0");
statement.setString(idx++, payload.extradata);
statement.setString(idx++, payload.haveOffer ? "1" : "0");
statement.setInt(idx++, payload.offerIdGroup);
statement.setInt(idx++, payload.limitedStack);
statement.setInt(idx++, payload.orderNumber);
statement.setInt(idx, offerId);
}
statement.execute();
if (statement.executeUpdate() == 0) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Offer not found: " + offerId));
return;
}
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Offer saved"));
@@ -74,13 +74,13 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
return;
}
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId);
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId, pageType);
if (parent == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
return;
}
if (this.wouldCreateCycle(pageId, parentId)) {
if (this.wouldCreateCycle(pageId, parentId, pageType)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to re-parent: that would create a cycle"));
return;
}
@@ -144,18 +144,21 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
statement.setInt(15, pageId);
}
statement.execute();
if (statement.executeUpdate() == 0) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page saved"));
}
private boolean wouldCreateCycle(int pageId, int parentId) {
private boolean wouldCreateCycle(int pageId, int parentId, CatalogPageType pageType) {
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);
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current, pageType);
if (parent == null) return false;
current = parent.getParentId();
}
@@ -57,8 +57,8 @@ public class FurniEditorDeleteEvent extends MessageHandler {
// Check catalog_items references
int catalogCount = 0;
try (PreparedStatement stmt = connection.prepareStatement(
"SELECT COUNT(*) FROM catalog_items WHERE item_ids LIKE ?")) {
stmt.setString(1, "%" + id + "%");
"SELECT COUNT(*) FROM catalog_items WHERE " + FurniEditorHelper.catalogItemIdsTokenSql("item_ids"))) {
stmt.setString(1, FurniEditorHelper.catalogItemIdsTokenPattern(id));
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
catalogCount = rs.getInt(1);
@@ -75,8 +75,8 @@ public class FurniEditorDetailEvent extends MessageHandler {
"ci.page_id AS ci_page_id, COALESCE(cp.caption, '') AS page_caption " +
"FROM catalog_items ci " +
"LEFT JOIN catalog_pages cp ON ci.page_id = cp.id " +
"WHERE ci.item_ids LIKE ?")) {
stmt.setString(1, "%" + itemId + "%");
"WHERE " + FurniEditorHelper.catalogItemIdsTokenSql("ci.item_ids"))) {
stmt.setString(1, FurniEditorHelper.catalogItemIdsTokenPattern(itemId));
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
catalogItems.add(FurniEditorHelper.readCatalogRef(rs));
@@ -11,6 +11,15 @@ import java.util.Map;
* FurniEditorSearchEvent to ensure consistent field reading.
*/
public class FurniEditorHelper {
public static final String CATALOG_ITEM_IDS_TOKEN_SQL = "CONCAT(',', REPLACE(item_ids, ' ', ''), ',') LIKE ?";
public static String catalogItemIdsTokenSql(String column) {
return "CONCAT(',', REPLACE(" + column + ", ' ', ''), ',') LIKE ?";
}
public static String catalogItemIdsTokenPattern(int itemId) {
return "%," + itemId + ",%";
}
/**
* Read the 14 base fields from items_base into a Map.
@@ -4,15 +4,11 @@ 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.furnieditor.FurniEditorResultComposer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class FurniEditorUpdateEvent extends MessageHandler {
@@ -39,57 +35,18 @@ public class FurniEditorUpdateEvent extends MessageHandler {
return;
}
if (json.size() == 0) {
this.client.sendResponse(new FurniEditorResultComposer(false, "No fields to update"));
FurniEditorUpdatePayload payload = FurniEditorUpdatePayload.validate(json);
if (!payload.valid()) {
this.client.sendResponse(new FurniEditorResultComposer(false, payload.error));
return;
}
// Build dynamic UPDATE with whitelisted fields
StringBuilder setClauses = new StringBuilder();
List<Object> values = new ArrayList<>();
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
String jsKey = entry.getKey();
String dbColumn = FurniEditorHelper.FIELD_MAP.get(jsKey);
if (dbColumn == null || !FurniEditorHelper.ALLOWED_UPDATE_FIELDS.contains(dbColumn)) {
continue; // Skip unknown or disallowed fields
}
if (setClauses.length() > 0) setClauses.append(", ");
setClauses.append("`").append(dbColumn).append("` = ?");
JsonElement val = entry.getValue();
if (val.isJsonPrimitive()) {
if (val.getAsJsonPrimitive().isBoolean()) {
values.add(val.getAsBoolean() ? "1" : "0");
} else if (val.getAsJsonPrimitive().isNumber()) {
// Check if it's a decimal number
String numStr = val.getAsString();
if (numStr.contains(".")) {
values.add(val.getAsDouble());
} else {
values.add(val.getAsInt());
}
} else {
values.add(val.getAsString());
}
} else {
values.add(val.toString());
}
}
if (setClauses.length() == 0) {
this.client.sendResponse(new FurniEditorResultComposer(false, "No valid fields to update"));
return;
}
String sql = "UPDATE items_base SET " + setClauses + " WHERE id = ?";
String sql = "UPDATE items_base SET " + payload.setClauses + " WHERE id = ?";
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = connection.prepareStatement(sql)) {
int idx = 1;
for (Object value : values) {
for (Object value : payload.values) {
if (value instanceof Integer) {
stmt.setInt(idx++, (Integer) value);
} else if (value instanceof Double) {
@@ -99,7 +56,10 @@ public class FurniEditorUpdateEvent extends MessageHandler {
}
}
stmt.setInt(idx, id);
stmt.executeUpdate();
if (stmt.executeUpdate() == 0) {
this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + id));
return;
}
}
// Reload emulator item definitions
@@ -5,6 +5,8 @@ import com.eu.habbo.habbohotel.items.FurnidataEntry;
import com.eu.habbo.habbohotel.items.FurnidataLock;
import com.eu.habbo.habbohotel.items.FurnidataWriter;
import com.eu.habbo.habbohotel.items.FurnitureTextProvider;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.FurnidataEntryBuilder;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
@@ -109,6 +111,7 @@ public class FurniEditorUpdateFurnidataEvent extends MessageHandler {
String safeDesc = (description != null) ? description : "";
boolean written;
boolean created = false;
List<FurnidataEntry> delta;
FurnidataLock.LOCK.lock();
@@ -121,8 +124,37 @@ public class FurniEditorUpdateFurnidataEvent extends MessageHandler {
);
written = writer.write(classname, safeName, safeDesc);
if (!written) {
this.client.sendResponse(new FurniEditorResultComposer(false, "Classname not found in furnidata"));
return;
// Upsert: no furnidata entry for this classname yet create a
// complete one seeded from items_base (id = sprite id).
Item item = Emulator.getGameEnvironment().getItemManager().getItem(itemId);
if (item == null) {
this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found"));
return;
}
String createTier = Emulator.getConfig().getValue("items.furnidata.create_tier", "custom");
String entry = FurnidataEntryBuilder.build(
item,
FurnitureTextProvider.sanitize(safeName),
FurnitureTextProvider.sanitize(safeDesc));
FurnidataWriter.CreateResult cr =
writer.create(item.getName(), item.getSpriteId(), item.getType(), entry, createTier);
switch (cr) {
case CREATED:
created = true;
written = true;
break;
case ALREADY_EXISTS:
// entry already present (race / no-op edit) apply the edit and treat as success
writer.write(classname, safeName, safeDesc);
written = true;
break;
case ID_COLLISION:
this.client.sendResponse(new FurniEditorResultComposer(false, "Sprite id already used by another classname"));
return;
default:
this.client.sendResponse(new FurniEditorResultComposer(false, "Failed to create furnidata entry"));
return;
}
}
delta = provider.reindexFromSource();
@@ -161,7 +193,7 @@ public class FurniEditorUpdateFurnidataEvent extends MessageHandler {
FurnidataAuditLog.record(
adminId,
classname,
"edit",
created ? "create" : "edit",
oldName != null ? oldName : "",
FurnitureTextProvider.sanitize(safeName),
oldDesc,
@@ -0,0 +1,133 @@
package com.eu.habbo.messages.incoming.furnieditor;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class FurniEditorUpdatePayload {
public final String setClauses;
public final List<Object> values;
public final String error;
private FurniEditorUpdatePayload(String setClauses, List<Object> values, String error) {
this.setClauses = setClauses;
this.values = values;
this.error = error;
}
public static FurniEditorUpdatePayload validate(JsonObject json) {
if (json == null || json.size() == 0) {
return invalid("No fields to update");
}
StringBuilder setClauses = new StringBuilder();
List<Object> values = new ArrayList<>();
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
String dbColumn = FurniEditorHelper.FIELD_MAP.get(entry.getKey());
if (dbColumn == null || !FurniEditorHelper.ALLOWED_UPDATE_FIELDS.contains(dbColumn)) {
continue;
}
Object value = validateValue(dbColumn, entry.getValue());
if (value == null) {
return invalid("Invalid value for " + entry.getKey());
}
if (setClauses.length() > 0) setClauses.append(", ");
setClauses.append("`").append(dbColumn).append("` = ?");
values.add(value);
}
if (setClauses.length() == 0) {
return invalid("No valid fields to update");
}
return new FurniEditorUpdatePayload(setClauses.toString(), values, null);
}
public boolean valid() {
return this.error == null;
}
private static FurniEditorUpdatePayload invalid(String error) {
return new FurniEditorUpdatePayload("", List.of(), error);
}
private static Object validateValue(String dbColumn, JsonElement element) {
if (element == null || element.isJsonNull() || !element.isJsonPrimitive()) {
return null;
}
JsonPrimitive primitive = element.getAsJsonPrimitive();
return switch (dbColumn) {
case "public_name" -> boundedString(primitive, 0, 56);
case "type" -> itemType(primitive);
case "width", "length" -> boundedInt(primitive, 0, 64);
case "stack_height" -> boundedDouble(primitive, 0.0D, 99.99D);
case "allow_stack", "allow_walk", "allow_sit", "allow_lay", "allow_gift",
"allow_trade", "allow_recycle", "allow_marketplace_sell", "allow_inventory_stack" -> booleanFlag(primitive);
case "interaction_type" -> boundedString(primitive, 0, 500);
case "interaction_modes_count" -> boundedInt(primitive, 0, 100);
case "vending_ids", "clothing_on_walk" -> boundedString(primitive, 0, 255);
case "customparams" -> boundedString(primitive, 0, 256);
case "multiheight" -> boundedString(primitive, 0, 50);
case "effect_id_male", "effect_id_female", "sprite_id" -> boundedInt(primitive, 0, Integer.MAX_VALUE);
case "description" -> boundedString(primitive, 0, 500);
default -> null;
};
}
private static String boundedString(JsonPrimitive primitive, int minLength, int maxLength) {
if (!primitive.isString()) return null;
String value = primitive.getAsString();
if (value.length() < minLength || value.length() > maxLength) return null;
return value;
}
private static String itemType(JsonPrimitive primitive) {
String value = boundedString(primitive, 1, 3);
if (value == null) return null;
return value.matches("[a-z]+") ? value : null;
}
private static Integer boundedInt(JsonPrimitive primitive, int min, int max) {
try {
int value = primitive.getAsInt();
return value >= min && value <= max ? value : null;
} catch (Exception e) {
return null;
}
}
private static Double boundedDouble(JsonPrimitive primitive, double min, double max) {
try {
double value = primitive.getAsDouble();
return Double.isFinite(value) && value >= min && value <= max ? value : null;
} catch (Exception e) {
return null;
}
}
private static String booleanFlag(JsonPrimitive primitive) {
if (primitive.isBoolean()) {
return primitive.getAsBoolean() ? "1" : "0";
}
if (primitive.isNumber()) {
int value = primitive.getAsInt();
return value == 0 || value == 1 ? String.valueOf(value) : null;
}
if (primitive.isString()) {
String value = primitive.getAsString();
return "0".equals(value) || "1".equals(value) ? value : null;
}
return null;
}
}
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.guilds;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.guilds.GuildBadgeBuilder;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
@@ -27,25 +28,8 @@ public class GuildChangeBadgeEvent extends MessageHandler {
int count = this.packet.readInt();
String badge = "";
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge += "b";
} else {
badge += "s";
}
badge += (id < 100 ? "0" : "") + (id < 10 ? "0" : "") + id + (color < 10 ? "0" : "") + color + "" + pos;
base += 3;
}
String badge = GuildBadgeBuilder.readBadge(this.packet, count);
if (badge == null) return;
if (guild.getBadge().equalsIgnoreCase(badge))
return;
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.guilds;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.guilds.GuildBadgeBuilder;
import com.eu.habbo.habbohotel.modtool.ScripterManager;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
@@ -69,24 +70,10 @@ public class RequestGuildBuyEvent extends MessageHandler {
int count = this.packet.readInt();
StringBuilder badge = new StringBuilder();
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge.append("b");
} else {
badge.append("s");
}
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
base += 3;
String badge = GuildBadgeBuilder.readBadge(this.packet, count);
if (badge == null) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
// Only charge the player once every step has been validated. Previously the
@@ -103,7 +90,7 @@ public class RequestGuildBuyEvent extends MessageHandler {
}
}
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge, colorOne, colorTwo);
r.setGuild(guild.getId());
r.removeAllRights();
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.polls;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.polls.Poll;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.users.AddUserBadgeComposer;
@@ -31,12 +32,20 @@ public class AnswerPollEvent extends MessageHandler {
if(answer.length() <= 0) return;
if (pollId == 0 && questionId <= 0) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().handleWordQuiz(this.client.getHabbo(), answer.toString());
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
if (room != null) {
room.handleWordQuiz(this.client.getHabbo(), answer.toString());
}
return;
}
answer = new StringBuilder(answer.substring(1));
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null || room.getPollId() != pollId) {
return;
}
Poll poll = Emulator.getGameEnvironment().getPollManager().getPoll(pollId);
if (poll != null) {
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.polls;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.polls.Poll;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,6 +18,10 @@ public class CancelPollEvent extends MessageHandler {
public void handle() throws Exception {
int pollId = this.packet.readInt();
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null || room.getPollId() != pollId) {
return;
}
Poll poll = Emulator.getGameEnvironment().getPollManager().getPoll(pollId);
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.polls;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.polls.Poll;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.polls.PollQuestionsComposer;
@@ -10,6 +11,11 @@ public class GetPollDataEvent extends MessageHandler {
public void handle() throws Exception {
int pollId = this.packet.readInt();
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null || room.getPollId() != pollId) {
return;
}
Poll poll = Emulator.getGameEnvironment().getPollManager().getPoll(pollId);
if (poll != null) {
@@ -36,6 +36,10 @@ public class RedeemClothingEvent extends MessageHandler {
if (clothing != null) {
if (!this.client.getHabbo().getInventory().getWardrobeComponent().getClothing().contains(clothing.id)) {
if (!this.grantClothing(clothing.id)) {
return;
}
item.setRoomId(0);
RoomTile tile = this.client.getHabbo().getHabboInfo().getCurrentRoom().getLayout().getTile(item.getX(), item.getY());
this.client.getHabbo().getHabboInfo().getCurrentRoom().removeHabboItem(item);
@@ -44,14 +48,6 @@ public class RedeemClothingEvent extends MessageHandler {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RemoveFloorItemComposer(item, true).compose());
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_clothing (user_id, clothing_id) VALUES (?, ?)")) {
statement.setInt(1, this.client.getHabbo().getHabboInfo().getId());
statement.setInt(2, clothing.id);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
this.client.getHabbo().getInventory().getWardrobeComponent().getClothing().add(clothing.id);
this.client.getHabbo().getInventory().getWardrobeComponent().getClothingSets().addAll(clothing.setId);
this.client.sendResponse(new UserClothesComposer(this.client.getHabbo()));
@@ -67,4 +63,15 @@ public class RedeemClothingEvent extends MessageHandler {
}
}
}
private boolean grantClothing(int clothingId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_clothing (user_id, clothing_id) VALUES (?, ?)")) {
statement.setInt(1, this.client.getHabbo().getHabboInfo().getId());
statement.setInt(2, clothingId);
return statement.executeUpdate() > 0;
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
return false;
}
}
}
@@ -103,6 +103,10 @@ public class ToggleFloorItemEvent extends MessageHandler {
// Do not move to onClick(). Wired could trigger it.
if (item instanceof InteractionMonsterPlantSeed) {
if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId()) {
return;
}
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
boolean isRare = item.getBaseItem().getName().contains("rare");
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.rooms.users;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.RoomManager;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
public class RoomUserBanEvent extends MessageHandler {
@@ -11,6 +12,18 @@ public class RoomUserBanEvent extends MessageHandler {
int roomId = this.packet.readInt();
String banName = this.packet.readString();
Emulator.getGameEnvironment().getRoomManager().banUserFromRoom(this.client.getHabbo(), userId, roomId, RoomManager.RoomBanTypes.valueOf(banName));
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null || room.getId() != roomId) {
return;
}
RoomManager.RoomBanTypes banType;
try {
banType = RoomManager.RoomBanTypes.valueOf(banName);
} catch (IllegalArgumentException e) {
return;
}
Emulator.getGameEnvironment().getRoomManager().banUserFromRoom(this.client.getHabbo(), userId, roomId, banType);
}
}
}
@@ -15,17 +15,18 @@ public class RoomUserMuteEvent extends MessageHandler {
int roomId = this.packet.readInt();
int minutes = this.packet.readInt();
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null || room.getId() != roomId) {
return;
}
if (room != null) {
if (room.hasRights(this.client.getHabbo()) || this.client.getHabbo().hasPermission("cmd_mute") || this.client.getHabbo().hasPermission(Permission.ACC_AMBASSADOR)) {
Habbo habbo = room.getHabbo(userId);
if (room.hasRights(this.client.getHabbo()) || this.client.getHabbo().hasPermission("cmd_mute") || this.client.getHabbo().hasPermission(Permission.ACC_AMBASSADOR)) {
Habbo habbo = room.getHabbo(userId);
if (habbo != null) {
room.muteHabbo(habbo, minutes);
habbo.getClient().sendResponse(new MutedWhisperComposer(minutes * 60));
AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("SelfModMuteSeen"));
}
if (habbo != null) {
room.muteHabbo(habbo, minutes);
habbo.getClient().sendResponse(new MutedWhisperComposer(minutes * 60));
AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("SelfModMuteSeen"));
}
}
}
@@ -3,8 +3,12 @@ package com.eu.habbo.messages.incoming.rooms.users;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.util.PacketGuard;
public class RoomUserRemoveRightsEvent extends MessageHandler {
private static final int MAX_RIGHTS_REMOVALS = 100;
private static final int BYTES_PER_USER_ID = 4;
@Override
public void handle() throws Exception {
int amount = this.packet.readInt();
@@ -15,6 +19,11 @@ public class RoomUserRemoveRightsEvent extends MessageHandler {
return;
if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
if (!PacketGuard.isCountInRange(amount, 1, MAX_RIGHTS_REMOVALS)
|| !PacketGuard.hasFixedWidthEntries(this.packet, amount, BYTES_PER_USER_ID)) {
return;
}
for (int i = 0; i < amount; i++) {
int userId = this.packet.readInt();
@@ -10,13 +10,13 @@ public class UnbanRoomUserEvent extends MessageHandler {
int userId = this.packet.readInt();
int roomId = this.packet.readInt();
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
if (room != null) {
if (room.isOwner(this.client.getHabbo())) {
room.unbanHabbo(userId);
}
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null || room.getId() != roomId) {
return;
}
if (room.isOwner(this.client.getHabbo())) {
room.unbanHabbo(userId);
}
}
}
@@ -514,7 +514,7 @@ public class Outgoing {
public final static int WiredOpenComposer = 1830;
public final static int UnknownCatalogPageOfferComposer = 1889;
public final static int NuxAlertComposer = 2023;
public final static int InClientLinkComposer = 2023;
public static int InClientLinkComposer = NuxAlertComposer;
public final static int HotelViewExpiringCatalogPageCommposer = 2515;
public final static int UnknownHabboWayQuizComposer = 2772;
public final static int PetLevelUpdatedComposer = 2824;
@@ -23,10 +23,10 @@ public class AvailableCommandsComposer extends MessageComposer {
for (Command cmd : this.commands) {
this.response.appendString(cmd.keys[0]);
this.response.appendString(
Emulator.getTexts().getValue("commands.description." + cmd.permission, cmd.permission)
Emulator.getTexts().getValueQuietly("commands.description." + cmd.permission, cmd.permission)
);
}
return this.response;
}
}
}
@@ -85,10 +85,10 @@ public abstract class Server {
}
if (!channelFuture.isSuccess()) {
LOGGER.info("Failed to connect to the host ({}:{})@{}", this.host, this.port, this.name);
LOGGER.info("Failed to start {} on {}:{}", this.name, this.host, this.port);
System.exit(0);
} else {
LOGGER.info("Started GameServer on {}:{}@{}", this.host, this.port, this.name);
LOGGER.info("Started {} on {}:{}", this.name, this.host, this.port);
}
}
@@ -100,7 +100,7 @@ public abstract class Server {
} catch(InterruptedException e) {
LOGGER.error("Exception during {} shutdown... HARD STOP", this.name, e);
}
LOGGER.info("GameServer Stopped!");
LOGGER.info("Stopped {}", this.name);
}
public ServerBootstrap getServerBootstrap() {
@@ -0,0 +1,25 @@
package com.eu.habbo.util;
import com.eu.habbo.messages.ClientMessage;
public final class PacketGuard {
private PacketGuard() {
}
public static boolean isCountInRange(int count, int min, int max) {
return count >= min && count <= max;
}
public static boolean hasReadableBytes(ClientMessage packet, int requiredBytes) {
return packet != null && requiredBytes >= 0 && packet.bytesAvailable() >= requiredBytes;
}
public static boolean hasFixedWidthEntries(ClientMessage packet, int entryCount, int bytesPerEntry) {
if (packet == null || entryCount < 0 || bytesPerEntry < 0) {
return false;
}
long requiredBytes = (long) entryCount * bytesPerEntry;
return requiredBytes <= Integer.MAX_VALUE && packet.bytesAvailable() >= requiredBytes;
}
}
@@ -0,0 +1,11 @@
package com.eu.habbo.util.logback;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
public class ConsoleLevelConverter extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
return ConsoleStyle.level(event == null ? null : event.getLevel(), ConsoleStyle.isRuntimeEnabled());
}
}
@@ -0,0 +1,11 @@
package com.eu.habbo.util.logback;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
public class ConsoleLoggerConverter extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
return ConsoleStyle.logger(event == null ? "" : event.getLoggerName(), ConsoleStyle.isRuntimeEnabled());
}
}
@@ -0,0 +1,106 @@
package com.eu.habbo.util.logback;
import ch.qos.logback.classic.Level;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
public final class ConsoleStyle {
private static final String ANSI_RESET = "\u001B[0m";
private static final String ANSI_BOLD = "\u001B[1m";
private static final String ANSI_DIM = "\u001B[2m";
private static final String ANSI_CYAN = "\u001B[36m";
private static final String ANSI_GREEN = "\u001B[32m";
private static final String ANSI_YELLOW = "\u001B[33m";
private static final String ANSI_RED = "\u001B[31m";
private static final int LOGGER_WIDTH = 22;
private ConsoleStyle() {
}
public static boolean isRuntimeEnabled() {
return isEnabled(
System.getenv(),
System.console() != null,
System.getProperty("os.name", "Unknown"),
System.getProperty("habbo.console.style", "auto"));
}
public static boolean isEnabled(Map<String, String> environment, boolean interactiveConsole, String osName, String styleProperty) {
String style = styleProperty == null ? "auto" : styleProperty.trim().toLowerCase(Locale.ROOT);
if (style.equals("ansi") || style.equals("color") || style.equals("colours") || style.equals("colors")) {
return true;
}
if (style.equals("plain") || style.equals("none") || style.equals("false") || style.equals("off")) {
return false;
}
if (!interactiveConsole) {
return false;
}
Map<String, String> env = environment == null ? Collections.emptyMap() : environment;
if (env.containsKey("NO_COLOR")) {
return false;
}
if (env.containsKey("WT_SESSION") || env.containsKey("ANSICON") || "ON".equalsIgnoreCase(env.get("ConEmuANSI"))) {
return true;
}
String term = env.getOrDefault("TERM", "");
if (term.equalsIgnoreCase("dumb")) {
return false;
}
if (!term.isBlank() && (term.contains("xterm") || term.contains("ansi") || term.contains("screen") || term.contains("tmux"))) {
return true;
}
return osName == null || !osName.toLowerCase(Locale.ROOT).startsWith("windows");
}
public static String level(Level level, boolean styled) {
String name = level == null ? "INFO" : level.toString();
String plain = String.format("%-5s", name);
if (!styled) {
return plain;
}
if (Level.ERROR.equals(level)) {
return ANSI_BOLD + ANSI_RED + "[x] " + plain + ANSI_RESET;
}
if (Level.WARN.equals(level)) {
return ANSI_YELLOW + "[!] " + plain + ANSI_RESET;
}
if (Level.DEBUG.equals(level) || Level.TRACE.equals(level)) {
return ANSI_DIM + "[.] " + plain + ANSI_RESET;
}
return ANSI_GREEN + "[i] " + plain + ANSI_RESET;
}
public static String logger(String loggerName, boolean styled) {
String compact = compactLoggerName(loggerName);
String plain = fit(compact, LOGGER_WIDTH);
return styled ? ANSI_CYAN + plain + ANSI_RESET : plain;
}
private static String compactLoggerName(String loggerName) {
if (loggerName == null || loggerName.isBlank()) {
return "";
}
int lastDot = loggerName.lastIndexOf('.');
return lastDot >= 0 ? loggerName.substring(lastDot + 1) : loggerName;
}
private static String fit(String value, int width) {
String safe = value == null ? "" : value;
if (safe.length() > width) {
return safe.substring(0, Math.max(0, width - 3)) + "...";
}
return String.format("%-" + width + "s", safe);
}
}