From 078fb3db60a9bce5e44b24452830251ae2867c79 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 21 Apr 2026 08:54:02 +0200 Subject: [PATCH 1/9] Fix wired text capture and showmessage behavior --- .../wired/effects/WiredEffectBotTalk.java | 2 +- .../effects/WiredEffectBotTalkToHabbo.java | 2 +- .../wired/effects/WiredEffectWhisper.java | 40 +++- .../core/WiredTextInputCaptureSupport.java | 180 +++++++++++++++++- 4 files changed, 213 insertions(+), 11 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java index d027210b..b4f20a8f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java @@ -82,7 +82,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { this.setDelay(delay); this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); - this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); + this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100))); this.mode = mode; return true; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java index 2673993c..c3bc7dfe 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java @@ -105,7 +105,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { throw new WiredSaveException("Delay too long"); this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); - this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); + this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100))); this.mode = mode; this.setDelay(delay); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java index 35e957c4..a2b858f4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java @@ -34,6 +34,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect { private static final long DELIVERY_DEDUP_TTL_MS = 60_000L; private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512; private static final ConcurrentHashMap DELIVERY_DEDUP = new ConcurrentHashMap<>(); + private static final int DEFAULT_SHOW_MESSAGE_MAX_LENGTH = 200; + private static final int DEFAULT_SHOW_MESSAGE_MAX_LINES = 8; protected String message = ""; protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; @@ -96,9 +98,12 @@ public class WiredEffectWhisper extends InteractionWiredEffect { if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) { message = Emulator.getGameEnvironment().getWordFilter().filter(message, null); - message = message.substring(0, Math.min(message.length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); } + int maxLength = Emulator.getConfig().getInt("hotel.wired.show_message.max_length", DEFAULT_SHOW_MESSAGE_MAX_LENGTH); + int maxLines = Emulator.getConfig().getInt("hotel.wired.show_message.max_lines", DEFAULT_SHOW_MESSAGE_MAX_LINES); + message = clampMessage(message, maxLength, maxLines); + int delay = settings.getDelay(); if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) @@ -109,6 +114,35 @@ public class WiredEffectWhisper extends InteractionWiredEffect { return true; } + private static String clampMessage(String value, int maxLength, int maxLines) { + if (value == null || value.isEmpty()) { + return ""; + } + + int safeMaxLength = Math.max(1, maxLength); + int safeMaxLines = Math.max(1, maxLines); + + String normalized = value.replace("\r\n", "\n").replace('\r', '\n'); + String[] lines = normalized.split("\n", -1); + + StringBuilder builder = new StringBuilder(); + int linesToWrite = Math.min(lines.length, safeMaxLines); + + for (int index = 0; index < linesToWrite; index++) { + if (builder.length() > 0) { + builder.append('\n'); + } + + builder.append(lines[index]); + } + + if (builder.length() > safeMaxLength) { + builder.setLength(safeMaxLength); + } + + return builder.toString(); + } + protected List resolveUsers(WiredContext ctx) { return WiredSourceUtil.resolveUsers(ctx, this.userSource); } @@ -212,7 +246,9 @@ public class WiredEffectWhisper extends InteractionWiredEffect { } String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo); - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.getBubble(this.bubbleStyle)))); + habbo.getClient().sendResponse(new RoomUserWhisperComposer( + new RoomChatMessage(msg, habbo.getRoomUnit(), RoomChatMessageBubbles.getBubble(this.bubbleStyle)) + )); if (habbo.getRoomUnit().isIdle()) { habbo.getRoomUnit().getRoom().unIdle(habbo); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java index cb73d427..1a6bc9d6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java @@ -64,8 +64,17 @@ public final class WiredTextInputCaptureSupport { return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch(); } - MatchResult matchResult = matchTemplate(trigger, text, capturersByName); + MatchResult matchResult = matchTemplate(trigger, text, capturersByName, room); if (!matchResult.matches) { + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] NO_MATCH room={} triggerId={} mode={} key='{}' text='{}' len={}", + room.getId(), + stack.triggerItem().getId(), + trigger.getMatchMode(), + safeForLog(trigger.getKey()), + safeForLog(text), + (text != null ? text.length() : 0)); + } return CaptureResult.noMatch(); } @@ -78,12 +87,28 @@ public final class WiredTextInputCaptureSupport { Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue()); if (resolvedValue == null) { + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] RESOLVE_FAIL room={} triggerId={} capturer='{}' raw='{}' rawLen={}", + room.getId(), + stack.triggerItem().getId(), + capture.getKey(), + safeForLog(capture.getValue()), + (capture.getValue() != null ? capture.getValue().length() : 0)); + } return CaptureResult.noMatch(); } capturedValues.put(capturer.getVariableItemId(), resolvedValue); } + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] MATCH_OK room={} triggerId={} captures={} textLen={}", + room.getId(), + stack.triggerItem().getId(), + capturedValues.size(), + (text != null ? text.length() : 0)); + } + return CaptureResult.matched(capturedValues); } @@ -108,12 +133,13 @@ public final class WiredTextInputCaptureSupport { return capturers; } - private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map capturersByName) { - String text = rawText != null ? rawText.trim() : ""; + private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map capturersByName, Room room) { + String text = rawText != null ? rawText : ""; + String normalizedText = text.trim(); String template = trigger.getKey() != null ? trigger.getKey().trim() : ""; if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) { - if (capturersByName.size() != 1 || text.isEmpty()) { + if (capturersByName.size() != 1 || normalizedText.isEmpty()) { return MatchResult.noMatch(); } @@ -123,12 +149,24 @@ public final class WiredTextInputCaptureSupport { return MatchResult.matched(captures); } + MatchResult adjacentCaptureResult = matchAdjacentCapturers(template, rawText, capturersByName, room, trigger.getMatchMode()); + if (adjacentCaptureResult != null) { + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] ADJACENT mode used key='{}' textLen={} matched={}", + safeForLog(template), + (rawText != null ? rawText.length() : 0), + adjacentCaptureResult.matches); + } + return adjacentCaptureResult; + } + TemplatePattern pattern = buildPattern(template); if (pattern == null) { return MatchResult.noMatch(); } - Matcher matcher = pattern.pattern.matcher(text); + String matchText = pattern.placeholderNames.isEmpty() ? normalizedText : text; + Matcher matcher = pattern.pattern.matcher(matchText); boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches(); if (!matches) { return MatchResult.noMatch(); @@ -142,12 +180,136 @@ public final class WiredTextInputCaptureSupport { } String capturedValue = matcher.group(index + 1); - captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : ""); + captures.put(placeholderName, normalizeCapturedValue(capturedValue)); } return MatchResult.matched(captures); } + private static MatchResult matchAdjacentCapturers(String template, String rawText, Map capturersByName, Room room, int matchMode) { + if (template == null || template.isEmpty() || rawText == null || capturersByName == null || capturersByName.isEmpty() || room == null) { + return null; + } + + Matcher matcher = PLACEHOLDER_PATTERN.matcher(template); + List placeholderNames = new ArrayList<>(); + int cursor = 0; + + while (matcher.find()) { + if (matcher.start() != cursor) { + return null; + } + + String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : ""; + if (placeholderName.isEmpty() || !capturersByName.containsKey(placeholderName)) { + return null; + } + + placeholderNames.add(placeholderName); + cursor = matcher.end(); + } + + if (placeholderNames.isEmpty() || cursor != template.length()) { + return null; + } + + int placeholderCount = placeholderNames.size(); + int textLength = rawText.length(); + + boolean[][] reachable = new boolean[placeholderCount + 1][textLength + 1]; + int[][] previousIndex = new int[placeholderCount + 1][textLength + 1]; + String[][] capturedValues = new String[placeholderCount + 1][textLength + 1]; + + for (int placeholderIndex = 0; placeholderIndex <= placeholderCount; placeholderIndex++) { + for (int textIndex = 0; textIndex <= textLength; textIndex++) { + previousIndex[placeholderIndex][textIndex] = -1; + } + } + + reachable[0][0] = true; + + for (int placeholderIndex = 0; placeholderIndex < placeholderCount; placeholderIndex++) { + String placeholderName = placeholderNames.get(placeholderIndex); + WiredExtraTextInputVariable capturer = capturersByName.get(placeholderName); + if (capturer == null) { + return MatchResult.noMatch(); + } + + for (int textIndex = 0; textIndex <= textLength; textIndex++) { + if (!reachable[placeholderIndex][textIndex]) { + continue; + } + + int minEndIndex = (textIndex < textLength) ? (textIndex + 1) : textIndex; + for (int endIndex = minEndIndex; endIndex <= textLength; endIndex++) { + if (reachable[placeholderIndex + 1][endIndex]) { + continue; + } + + String candidate = rawText.substring(textIndex, endIndex); + if (capturer.resolveCapturedValue(room, candidate) == null) { + continue; + } + + reachable[placeholderIndex + 1][endIndex] = true; + previousIndex[placeholderIndex + 1][endIndex] = textIndex; + capturedValues[placeholderIndex + 1][endIndex] = candidate; + } + } + } + + int resultEndIndex = -1; + if (matchMode == MATCH_CONTAINS) { + for (int endIndex = textLength; endIndex >= 0; endIndex--) { + if (reachable[placeholderCount][endIndex]) { + resultEndIndex = endIndex; + break; + } + } + } else if (reachable[placeholderCount][textLength]) { + resultEndIndex = textLength; + } + + if (resultEndIndex < 0) { + return MatchResult.noMatch(); + } + + LinkedHashMap captures = new LinkedHashMap<>(); + int backtrackTextIndex = resultEndIndex; + for (int placeholderIndex = placeholderCount; placeholderIndex > 0; placeholderIndex--) { + String placeholderName = placeholderNames.get(placeholderIndex - 1); + String capturedValue = capturedValues[placeholderIndex][backtrackTextIndex]; + captures.put(placeholderName, capturedValue != null ? capturedValue : ""); + backtrackTextIndex = previousIndex[placeholderIndex][backtrackTextIndex]; + if (backtrackTextIndex < 0) { + return MatchResult.noMatch(); + } + } + + return MatchResult.matched(captures); + } + + private static String normalizeCapturedValue(String value) { + return value != null ? value : ""; + } + + private static String safeForLog(String value) { + if (value == null) { + return ""; + } + + String normalized = value + .replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\u00A0", "⍽"); + + if (normalized.length() > 180) { + return normalized.substring(0, 180) + "...(" + normalized.length() + ")"; + } + + return normalized; + } + private static TemplatePattern buildPattern(String template) { if (template == null || template.isEmpty()) { return null; @@ -160,7 +322,7 @@ public final class WiredTextInputCaptureSupport { while (matcher.find()) { regex.append(Pattern.quote(template.substring(cursor, matcher.start()))); - regex.append("(.+?)"); + regex.append(hasPlaceholderAfter(template, matcher.end()) ? "(.+?)" : "(.+)"); String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : ""; placeholderNames.add(placeholderName); @@ -176,6 +338,10 @@ public final class WiredTextInputCaptureSupport { return new TemplatePattern(Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE), placeholderNames); } + private static boolean hasPlaceholderAfter(String template, int cursor) { + return PLACEHOLDER_PATTERN.matcher(template.substring(cursor)).find(); + } + public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) { if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) { return; From 8bbe8640b0c655c9ef9017ecb678098472e29c52 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 21 Apr 2026 11:13:32 +0200 Subject: [PATCH 2/9] WIP preserve local changes before duckie merge --- .../sqlupdates/custom_nick_icons_setup.sql | 36 ++ Emulator/sqlupdates/custom_prefixes_setup.sql | 356 ++++++++++--- .../sqlupdates/wired_message_length_512.sql | 3 + .../eu/habbo/habbohotel/GameEnvironment.java | 10 + .../eu/habbo/habbohotel/bots/BotManager.java | 22 +- .../habbohotel/catalog/CatalogManager.java | 2 +- .../WiredConditionSelectionQuantity.java | 167 ++++++- .../effects/WiredEffectControlClock.java | 86 +++- .../effects/WiredEffectFurniToFurni.java | 67 ++- .../wired/effects/WiredEffectSendSignal.java | 13 +- .../extra/WiredExtraTextInputVariable.java | 12 +- .../WiredExtraVariableTextConnector.java | 32 +- .../selector/WiredEffectFurniAltitude.java | 5 + .../wired/selector/WiredEffectFurniArea.java | 5 + .../selector/WiredEffectFurniByType.java | 5 + .../WiredEffectFurniNeighborhood.java | 69 ++- .../selector/WiredEffectFurniOnFurni.java | 5 + .../wired/selector/WiredEffectFurniPicks.java | 5 + .../selector/WiredEffectFurniSignal.java | 5 + .../wired/selector/WiredEffectUsersArea.java | 5 + .../selector/WiredEffectUsersByAction.java | 5 + .../selector/WiredEffectUsersByName.java | 5 + .../selector/WiredEffectUsersByType.java | 5 + .../wired/selector/WiredEffectUsersGroup.java | 5 + .../selector/WiredEffectUsersHandItem.java | 5 + .../WiredEffectUsersNeighborhood.java | 71 ++- .../selector/WiredEffectUsersOnFurni.java | 5 + .../selector/WiredEffectUsersSignal.java | 5 + .../wired/selector/WiredEffectUsersTeam.java | 5 + .../WiredEffectVariableSelectorBase.java | 5 + .../eu/habbo/habbohotel/pets/PetManager.java | 46 +- .../habbohotel/rooms/RoomChatMessage.java | 26 +- .../habbohotel/rooms/RoomSpecialTypes.java | 12 +- .../translations/GoogleTranslateManager.java | 469 ++++++++++++++++++ .../habbohotel/users/HabboInventory.java | 34 ++ .../users/UserCustomizationData.java | 121 +++++ .../habbo/habbohotel/users/UserNickIcon.java | 118 +++++ .../eu/habbo/habbohotel/users/UserPrefix.java | 78 ++- .../users/inventory/NickIconsComponent.java | 119 +++++ .../users/inventory/PrefixesComponent.java | 9 + .../UserVisualSettingsComponent.java | 94 ++++ .../habbohotel/wired/api/IWiredEffect.java | 16 + .../habbohotel/wired/core/WiredContext.java | 2 +- .../habbohotel/wired/core/WiredEngine.java | 113 ++++- .../wired/core/WiredSourceUtil.java | 16 +- .../wired/core/WiredTextPlaceholderUtil.java | 42 +- .../WiredVariableTextConnectorSupport.java | 17 +- .../com/eu/habbo/messages/PacketManager.java | 16 + .../eu/habbo/messages/incoming/Incoming.java | 7 + .../nickicons/PurchaseNickIconEvent.java | 95 ++++ .../nickicons/RequestUserNickIconsEvent.java | 11 + .../nickicons/SetActiveNickIconEvent.java | 34 ++ .../prefixes/PurchaseCatalogPrefixEvent.java | 84 ++++ .../prefixes/PurchasePrefixEvent.java | 68 ++- .../prefixes/SetActivePrefixEvent.java | 12 + .../prefixes/SetDisplayOrderEvent.java | 26 + .../TranslationLanguagesRequestEvent.java | 28 ++ .../TranslationTextRequestEvent.java | 25 + .../wired/WiredEffectSaveDataEvent.java | 11 + .../eu/habbo/messages/outgoing/Outgoing.java | 3 + .../nickicons/UserNickIconsComposer.java | 217 ++++++++ .../prefixes/ActivePrefixUpdatedComposer.java | 2 + .../prefixes/PrefixReceivedComposer.java | 1 + .../prefixes/UserPrefixesComposer.java | 1 + .../rooms/users/RoomUserDataComposer.java | 9 + .../rooms/users/RoomUsersComposer.java | 17 + .../TranslationLanguagesComposer.java | 33 ++ .../TranslationResultComposer.java | 29 ++ .../outgoing/users/UserProfileComposer.java | 9 + .../java/com/eu/habbo/networking/Server.java | 2 +- .../com/eu/habbo/plugin/PluginManager.java | 2 + 71 files changed, 2853 insertions(+), 247 deletions(-) create mode 100644 Emulator/sqlupdates/custom_nick_icons_setup.sql create mode 100644 Emulator/sqlupdates/wired_message_length_512.sql create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/translations/GoogleTranslateManager.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserCustomizationData.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserNickIcon.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/NickIconsComponent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/UserVisualSettingsComponent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/PurchaseNickIconEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/RequestUserNickIconsEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/SetActiveNickIconEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchaseCatalogPrefixEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetDisplayOrderEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationLanguagesRequestEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationTextRequestEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/nickicons/UserNickIconsComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationLanguagesComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationResultComposer.java diff --git a/Emulator/sqlupdates/custom_nick_icons_setup.sql b/Emulator/sqlupdates/custom_nick_icons_setup.sql new file mode 100644 index 00000000..dbf9f81a --- /dev/null +++ b/Emulator/sqlupdates/custom_nick_icons_setup.sql @@ -0,0 +1,36 @@ +-- ============================================================ +-- Nick Icon Customization Setup +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `icon_key` VARCHAR(50) NOT NULL, + `display_name` VARCHAR(100) NOT NULL DEFAULT '', + `points` INT(11) NOT NULL DEFAULT 0, + `points_type` INT(11) NOT NULL DEFAULT 0, + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_icon_key` (`icon_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `user_nick_icons` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NOT NULL, + `icon_key` VARCHAR(50) NOT NULL, + `active` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_icon` (`user_id`, `icon_key`), + KEY `idx_user_id` (`user_id`), + KEY `idx_user_active` (`user_id`, `active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES + ('1', 'Icon 1', 10, 0, 1, 1), + ('2', 'Icon 2', 10, 0, 1, 2), + ('3', 'Icon 3', 10, 0, 1, 3), + ('4', 'Icon 4', 10, 0, 1, 4), + ('5', 'Icon 5', 10, 0, 1, 5), + ('6', 'Icon 6', 10, 0, 1, 6); +ALTER TABLE `custom_nick_icons_catalog` + ADD COLUMN IF NOT EXISTS `display_name` VARCHAR(100) NOT NULL DEFAULT '' AFTER `icon_key`; diff --git a/Emulator/sqlupdates/custom_prefixes_setup.sql b/Emulator/sqlupdates/custom_prefixes_setup.sql index 7d5b22c5..dd27c7d0 100644 --- a/Emulator/sqlupdates/custom_prefixes_setup.sql +++ b/Emulator/sqlupdates/custom_prefixes_setup.sql @@ -1,8 +1,13 @@ -- ============================================================ --- Custom Prefix System - Complete Setup +-- Custom Prefix System - Complete Setup (safe upgrade version) -- ============================================================ +-- Questo script è pensato per essere rieseguito senza errori +-- anche se le tabelle esistono già con una struttura parziale. + +-- ------------------------------------------------------------ -- 1. Main user prefixes table +-- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS `user_prefixes` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, @@ -10,28 +15,57 @@ CREATE TABLE IF NOT EXISTS `user_prefixes` ( `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', `icon` VARCHAR(50) NOT NULL DEFAULT '', `effect` VARCHAR(50) NOT NULL DEFAULT '', + `font` VARCHAR(50) NOT NULL DEFAULT '', + `catalog_prefix_id` INT(11) NOT NULL DEFAULT 0, + `display_name` VARCHAR(100) NOT NULL DEFAULT '', + `points` INT(11) NOT NULL DEFAULT 0, + `points_type` INT(11) NOT NULL DEFAULT 0, + `is_custom` TINYINT(1) NOT NULL DEFAULT 1, `active` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), INDEX `idx_user_id` (`user_id`), INDEX `idx_user_active` (`user_id`, `active`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 2. Prefix settings table +-- ------------------------------------------------------------ +-- 2. Catalog table +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `display_name` VARCHAR(100) NOT NULL DEFAULT '', + `text` VARCHAR(50) NOT NULL, + `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', + `icon` VARCHAR(50) NOT NULL DEFAULT '', + `effect` VARCHAR(50) NOT NULL DEFAULT '', + `font` VARCHAR(50) NOT NULL DEFAULT '', + `points` INT(11) NOT NULL DEFAULT 0, + `points_type` INT(11) NOT NULL DEFAULT 0, + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ------------------------------------------------------------ +-- 3. User visual settings +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS `user_visual_settings` ( + `user_id` INT(11) NOT NULL, + `display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name', + PRIMARY KEY (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ------------------------------------------------------------ +-- 4. Prefix settings table +-- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` ( `key_name` VARCHAR(100) NOT NULL, `value` VARCHAR(255) NOT NULL, PRIMARY KEY (`key_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- Default settings -INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES - ('max_length', '15'), - ('min_rank_to_buy', '1'), - ('price_credits', '5'), - ('price_points', '0'), - ('points_type', '0'); - --- 3. Blacklisted words table +-- ------------------------------------------------------------ +-- 5. Blacklist table +-- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `word` VARCHAR(100) NOT NULL, @@ -39,77 +73,245 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` ( UNIQUE KEY `uk_word` (`word`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- Example blacklist entries (customize as needed) +-- ============================================================ +-- Schema upgrades for existing installations +-- ============================================================ + +-- ------------------------------------------------------------ +-- user_prefixes: add missing columns safely +-- ------------------------------------------------------------ + +SET @dbname = DATABASE(); + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'font' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'catalog_prefix_id' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `catalog_prefix_id` INT(11) NOT NULL DEFAULT 0 AFTER `font`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'display_name' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `display_name` VARCHAR(100) NOT NULL DEFAULT '''' AFTER `catalog_prefix_id`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'points' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `display_name`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'points_type' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'is_custom' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `is_custom` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ------------------------------------------------------------ +-- custom_prefixes_catalog: add missing columns safely +-- ------------------------------------------------------------ + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'font' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'points' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `font`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'points_type' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'enabled' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'sort_order' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `sort_order` INT(11) NOT NULL DEFAULT 0 AFTER `enabled`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ============================================================ +-- Default settings +-- ============================================================ +INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES + ('max_length', '15'), + ('min_rank_to_buy', '1'), + ('price_credits', '5'), + ('price_points', '0'), + ('points_type', '0'), + ('font_price_credits', '10'), + ('font_price_points', '0'), + ('font_points_type', '0'); + +-- ============================================================ +-- Default catalog entries +-- ============================================================ +INSERT IGNORE INTO `custom_prefixes_catalog` +(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`) VALUES + (1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1), + (2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2), + (3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3); + +-- ============================================================ +-- Example blacklist entries +-- ============================================================ INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES ('admin'), ('staff'), ('mod'), ('owner'); --- 4. Add effect column (if table already exists without it) --- ALTER TABLE `user_prefixes` ADD COLUMN IF NOT EXISTS `effect` VARCHAR(50) NOT NULL DEFAULT '' AFTER `icon`; - -- ============================================================ --- Catalog page for custom prefixes +-- Notes -- ============================================================ --- NOTE: Adjust parent_id to match your catalog parent category ID. --- Example: parent_id = -1 for root, or the ID of your "Extra" / "Specials" category - -INSERT INTO `catalog_pages` ( - `parent_id`, `caption`, `caption_save`, `icon_image`, `visible`, `enabled`, - `min_rank`, `page_layout`, `page_strings_1`, `page_strings_2` -) VALUES ( - -1, - 'Custom Prefix', - 'custom_prefix', - 1, - 1, - 1, - 1, - 'custom_prefix', - 'Create your own custom prefix!\rChoose text, colors, icon and effects to stand out in chat.', - '' -); - --- ============================================================ --- Command texts (insert into emulator_texts if not present) --- ============================================================ -INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES - -- GivePrefix command - ('commands.keys.cmd_give_prefix', 'giveprefix'), - ('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix [icon] [effect]'), - ('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'), - ('commands.error.cmd_give_prefix.too_long', 'Prefix text is too long (max 15 characters).'), - ('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'), - ('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'), - -- ListPrefixes command - ('commands.keys.cmd_list_prefixes', 'listprefixes'), - ('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes '), - ('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'), - ('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'), - ('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'), - -- RemovePrefix command - ('commands.keys.cmd_remove_prefix', 'removeprefix'), - ('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix '), - ('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'), - ('commands.error.cmd_remove_prefix.invalid_id', 'Invalid prefix ID. Must be a number or "all".'), - ('commands.error.cmd_remove_prefix.not_found', 'Prefix not found for this user.'), - ('commands.succes.cmd_remove_prefix', 'Prefix #%id% removed from %user%.'), - ('commands.succes.cmd_remove_prefix.all', 'All prefixes removed from %user%.'), - -- PrefixBlacklist command - ('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'), - ('commands.error.cmd_prefix_blacklist.usage', 'Usage: :prefixblacklist [word]'), - ('commands.error.cmd_prefix_blacklist.empty_word', 'Word cannot be empty.'), - ('commands.succes.cmd_prefix_blacklist.header', 'Blacklisted prefix words:'), - ('commands.succes.cmd_prefix_blacklist.empty', 'No blacklisted words.'), - ('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'), - ('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.'); - --- ============================================================ --- Permissions for prefix commands (add to permissions table) --- ============================================================ -INSERT IGNORE INTO `permissions` (`id`, `rank_id`, `permission_name`, `setting_type`) VALUES - (NULL, 7, 'cmd_give_prefix', '1'), - (NULL, 7, 'cmd_list_prefixes', '1'), - (NULL, 7, 'cmd_remove_prefix', '1'), - (NULL, 7, 'cmd_prefix_blacklist', '1'); +-- Preset prefixes for `:customize` are loaded directly by +-- UserNickIconsComposer and displayed inside the `:customize` panel. +-- +-- This setup does not require rows in `catalog_pages`. +-- +-- Command texts / permission inserts are intentionally omitted +-- for compatibility with both legacy and normalized permission schemas. \ No newline at end of file diff --git a/Emulator/sqlupdates/wired_message_length_512.sql b/Emulator/sqlupdates/wired_message_length_512.sql new file mode 100644 index 00000000..ad23b30f --- /dev/null +++ b/Emulator/sqlupdates/wired_message_length_512.sql @@ -0,0 +1,3 @@ +INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) +VALUES ('hotel.wired.message.max_length', '512', 'Maximum length of text fields used by wired messages and bot text effects.') +ON DUPLICATE KEY UPDATE `value` = '512'; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java index 879e2d6b..a3899b74 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java @@ -21,6 +21,7 @@ import com.eu.habbo.habbohotel.pets.PetManager; import com.eu.habbo.habbohotel.polls.PollManager; import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager; import com.eu.habbo.habbohotel.rooms.RoomManager; +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionManager; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionScheduler; @@ -58,6 +59,7 @@ public class GameEnvironment { private SubscriptionManager subscriptionManager; private CalendarManager calendarManager; private RoomChatBubbleManager roomChatBubbleManager; + private GoogleTranslateManager googleTranslateManager; public void load() throws Exception { LOGGER.info("GameEnvironment -> Loading..."); @@ -84,6 +86,7 @@ public class GameEnvironment { this.pollManager = new PollManager(); this.calendarManager = new CalendarManager(); this.roomChatBubbleManager = new RoomChatBubbleManager(); + this.googleTranslateManager = new GoogleTranslateManager(); this.roomManager.loadPublicRooms(); this.navigatorManager.loadNavigator(); @@ -121,6 +124,9 @@ public class GameEnvironment { this.hotelViewManager.dispose(); this.subscriptionManager.dispose(); this.calendarManager.dispose(); + if (this.googleTranslateManager != null) { + this.googleTranslateManager.clearCache(); + } LOGGER.info("GameEnvironment -> Disposed!"); } @@ -219,4 +225,8 @@ public class GameEnvironment { public RoomChatBubbleManager getRoomChatBubbleManager() { return roomChatBubbleManager; } + + public GoogleTranslateManager getGoogleTranslateManager() { + return this.googleTranslateManager; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java index 6c0907ff..80f820d3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java @@ -71,13 +71,23 @@ public class BotManager { } public Bot createBot(THashMap data, String type) { + return this.createBot(data, type, 0); + } + + public Bot createBot(THashMap data, String type, int ownerId) { + if (ownerId <= 0) { + LOGGER.error("Cannot create bot of type '{}' without a valid owner user id.", type); + return null; + } + Bot bot = null; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (0, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { - statement.setString(1, data.get("name")); - statement.setString(2, data.get("motto")); - statement.setString(3, data.get("figure")); - statement.setString(4, data.get("gender").toUpperCase()); - statement.setString(5, type); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (?, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, ownerId); + statement.setString(2, data.get("name")); + statement.setString(3, data.get("motto")); + statement.setString(4, data.get("figure")); + statement.setString(5, data.get("gender").toUpperCase()); + statement.setString(6, type); statement.execute(); try (ResultSet set = statement.getGeneratedKeys()) { if (set.next()) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 9f937c66..00e10c05 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -1058,7 +1058,7 @@ public class CatalogManager { } } - Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type); + Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type, habbo.getHabboInfo().getId()); if (bot != null) { bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java index 590ea9d8..5ed0e938 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java @@ -1,5 +1,6 @@ package com.eu.habbo.habbohotel.items.interactions.wired.conditions; +import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; @@ -11,10 +12,12 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.stream.Collectors; public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private static final int COMPARISON_LESS_THAN = 0; @@ -23,9 +26,16 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private static final int SOURCE_GROUP_USERS = 0; private static final int SOURCE_GROUP_FURNI = 1; + private static final int SOURCE_USER_TRIGGER = 0; + private static final int SOURCE_USER_SIGNAL = 1; + private static final int SOURCE_USER_CLICKED = 2; + private static final int SOURCE_FURNI_TRIGGER = 3; + private static final int SOURCE_FURNI_PICKED = 4; + private static final int SOURCE_FURNI_SIGNAL = 5; public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY; + private final THashSet items; private int comparison = COMPARISON_EQUAL; private int quantity = 0; private int sourceGroup = SOURCE_GROUP_USERS; @@ -33,10 +43,12 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); + this.items = new THashSet<>(); } public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); } @Override @@ -46,9 +58,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { @Override public void serializeWiredData(ServerMessage message, Room room) { - message.appendBoolean(false); - message.appendInt(5); - message.appendInt(0); + this.refresh(room); + + boolean pickMode = this.sourceGroup == SOURCE_GROUP_FURNI && this.sourceType == WiredSourceUtil.SOURCE_SELECTED; + + message.appendBoolean(pickMode); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(pickMode ? this.items.size() : 0); + if (pickMode) { + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + } message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); @@ -69,8 +90,36 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0; - this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS; - this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER; + this.items.clear(); + + if (params.length > 3) { + this.sourceGroup = this.normalizeSourceGroup(params[2]); + this.sourceType = this.normalizeSourceType(this.sourceGroup, params[3]); + } else { + this.setSourceSelection((params.length > 2) ? params[2] : SOURCE_USER_TRIGGER); + } + + if (this.sourceGroup != SOURCE_GROUP_FURNI || this.sourceType != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + int count = settings.getFurniIds().length; + if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } return true; } @@ -97,11 +146,14 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { @Override public String getWiredData() { + this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); + return WiredManager.getGson().toJson(new JsonData( this.comparison, this.quantity, this.sourceGroup, - this.sourceType + this.sourceType, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) )); } @@ -125,6 +177,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { this.quantity = this.normalizeQuantity(data.quantity); this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup); this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType); + this.loadSelectedItems(data.itemIds, room); return; } @@ -150,6 +203,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { @Override public void onPickUp() { + this.items.clear(); this.comparison = COMPARISON_EQUAL; this.quantity = 0; this.sourceGroup = SOURCE_GROUP_USERS; @@ -158,7 +212,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private int resolveCount(WiredContext ctx) { if (this.sourceGroup == SOURCE_GROUP_FURNI) { - List items = WiredSourceUtil.resolveItems(ctx, this.sourceType, null); + List items = WiredSourceUtil.resolveItems(ctx, this.sourceType, this.items); return items.size(); } @@ -188,10 +242,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private int normalizeSourceType(int group, int value) { if (group == SOURCE_GROUP_USERS) { - return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + switch (value) { + case WiredSourceUtil.SOURCE_CLICKED_USER: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_SELECTOR: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } } switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: case WiredSourceUtil.SOURCE_SELECTOR: case WiredSourceUtil.SOURCE_SIGNAL: case WiredSourceUtil.SOURCE_TRIGGER: @@ -201,17 +263,104 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { } } + private int getSourceSelection() { + if (this.sourceGroup == SOURCE_GROUP_FURNI) { + switch (this.sourceType) { + case WiredSourceUtil.SOURCE_SELECTED: + return SOURCE_FURNI_PICKED; + case WiredSourceUtil.SOURCE_SIGNAL: + return SOURCE_FURNI_SIGNAL; + default: + return SOURCE_FURNI_TRIGGER; + } + } + + switch (this.sourceType) { + case WiredSourceUtil.SOURCE_CLICKED_USER: + return SOURCE_USER_CLICKED; + case WiredSourceUtil.SOURCE_SIGNAL: + return SOURCE_USER_SIGNAL; + default: + return SOURCE_USER_TRIGGER; + } + } + + private void setSourceSelection(int value) { + switch (value) { + case SOURCE_USER_SIGNAL: + this.sourceGroup = SOURCE_GROUP_USERS; + this.sourceType = WiredSourceUtil.SOURCE_SIGNAL; + break; + case SOURCE_USER_CLICKED: + this.sourceGroup = SOURCE_GROUP_USERS; + this.sourceType = WiredSourceUtil.SOURCE_CLICKED_USER; + break; + case SOURCE_FURNI_TRIGGER: + this.sourceGroup = SOURCE_GROUP_FURNI; + this.sourceType = WiredSourceUtil.SOURCE_TRIGGER; + break; + case SOURCE_FURNI_PICKED: + this.sourceGroup = SOURCE_GROUP_FURNI; + this.sourceType = WiredSourceUtil.SOURCE_SELECTED; + break; + case SOURCE_FURNI_SIGNAL: + this.sourceGroup = SOURCE_GROUP_FURNI; + this.sourceType = WiredSourceUtil.SOURCE_SIGNAL; + break; + default: + this.sourceGroup = SOURCE_GROUP_USERS; + this.sourceType = WiredSourceUtil.SOURCE_TRIGGER; + break; + } + } + + private void loadSelectedItems(List itemIds, Room room) { + this.items.clear(); + + if (itemIds == null || room == null) { + return; + } + + for (Integer itemId : itemIds) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + } + + private void refresh(Room room) { + if (room == null || this.items.isEmpty()) { + return; + } + + THashSet itemsToRemove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (item == null || room.getHabboItem(item.getId()) == null) { + itemsToRemove.add(item); + } + } + + for (HabboItem item : itemsToRemove) { + this.items.remove(item); + } + } + static class JsonData { int comparison; int quantity; int sourceGroup; int sourceType; + List itemIds; - public JsonData(int comparison, int quantity, int sourceGroup, int sourceType) { + public JsonData(int comparison, int quantity, int sourceGroup, int sourceType, List itemIds) { this.comparison = comparison; this.quantity = quantity; this.sourceGroup = sourceGroup; this.sourceType = sourceType; + this.itemIds = itemIds; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java index b4f55831..92a8a1f7 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer; import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; @@ -60,29 +61,74 @@ public class WiredEffectControlClock extends InteractionWiredEffect { } for (HabboItem item : effectiveItems) { - if (!(item instanceof InteractionGameUpCounter)) { + if (!(item instanceof InteractionGameTimer)) { continue; } - InteractionGameUpCounter counter = (InteractionGameUpCounter) item; - - switch (this.action) { - case ACTION_START: - counter.restartFromZero(room); - break; - case ACTION_STOP: - counter.stopCounter(room); - break; - case ACTION_RESET: - counter.resetCounter(room); - break; - case ACTION_PAUSE: - counter.pauseCounter(room); - break; - case ACTION_RESUME: - counter.resumeCounter(room); - break; + if (item instanceof InteractionGameUpCounter) { + this.controlUpCounter((InteractionGameUpCounter) item, room); + continue; } + + this.controlGameTimer((InteractionGameTimer) item, room); + } + } + + private void controlUpCounter(InteractionGameUpCounter counter, Room room) { + switch (this.action) { + case ACTION_START: + counter.restartFromZero(room); + break; + case ACTION_STOP: + counter.stopCounter(room); + break; + case ACTION_RESET: + counter.resetCounter(room); + break; + case ACTION_PAUSE: + counter.pauseCounter(room); + break; + case ACTION_RESUME: + counter.resumeCounter(room); + break; + } + } + + private void controlGameTimer(InteractionGameTimer timer, Room room) { + switch (this.action) { + case ACTION_START: + timer.startTimer(room); + break; + case ACTION_STOP: + this.stopGameTimer(timer, room, false); + break; + case ACTION_RESET: + this.stopGameTimer(timer, room, true); + break; + case ACTION_PAUSE: + timer.pauseTimer(room); + break; + case ACTION_RESUME: + timer.resumeTimer(room); + break; + } + } + + private void stopGameTimer(InteractionGameTimer timer, Room room, boolean resetTime) { + boolean wasActive = timer.isRunning() || timer.isPaused(); + + timer.endGame(room); + + if (resetTime) { + timer.setTimeNow(timer.getBaseTime()); + timer.setExtradata(timer.getTimeNow() + "\t" + timer.getBaseTime()); + } + + room.updateItem(timer); + timer.needsUpdate(true); + + if (wasActive) { + WiredManager.triggerGameEnds(room); } } @@ -206,7 +252,7 @@ public class WiredEffectControlClock extends InteractionWiredEffect { throw new WiredSaveException(String.format("Item %s not found", itemId)); } - if (!(item instanceof InteractionGameUpCounter)) { + if (!(item instanceof InteractionGameTimer)) { throw new WiredSaveException("wiredfurni.error.require_counter_furni"); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java index ed8bf0a9..265ad507 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java @@ -53,26 +53,37 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect { return; } - HabboItem moveItem = this.resolveLastMoveItem(ctx); - HabboItem targetItem = this.resolveLastTargetItem(ctx); + List moveItems = this.resolveMoveItems(ctx); + List targetItems = this.resolveTargetItems(ctx); - if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) { + if (moveItems.isEmpty() || targetItems.isEmpty()) { return; } - RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); - if (targetTile == null) { - return; - } + int targetIndex = 0; + for (HabboItem moveItem : moveItems) { + if (moveItem == null) { + continue; + } - FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx); - if (error == FurnitureMovementError.NONE) { - return; - } + HabboItem targetItem = targetItems.get(targetIndex % targetItems.size()); + targetIndex++; - error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx); - if (error == FurnitureMovementError.NONE) { - return; + if (targetItem == null || moveItem.getId() == targetItem.getId()) { + continue; + } + + RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); + if (targetTile == null) { + continue; + } + + FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx); + if (error == FurnitureMovementError.NONE) { + continue; + } + + WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx); } } @@ -233,35 +244,23 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect { return COOLDOWN_MOVEMENT; } - private HabboItem resolveLastMoveItem(WiredContext ctx) { - return this.resolveLastItem(ctx, this.moveSource, this.moveItems); + private List resolveMoveItems(WiredContext ctx) { + return this.resolveItems(ctx, this.moveSource, this.moveItems); } - private HabboItem resolveLastTargetItem(WiredContext ctx) { + private List resolveTargetItems(WiredContext ctx) { int source = (this.targetSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.targetSource; - return this.resolveLastItem(ctx, source, this.targetItems); + return this.resolveItems(ctx, source, this.targetItems); } - private HabboItem resolveLastItem(WiredContext ctx, int source, List items) { + private List resolveItems(WiredContext ctx, int source, List items) { if (source == WiredSourceUtil.SOURCE_SELECTED) { this.validateItems(items); } - List resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items); - - if (resolvedItems.isEmpty()) { - return null; - } - - for (int index = resolvedItems.size() - 1; index >= 0; index--) { - HabboItem item = resolvedItems.get(index); - - if (item != null) { - return item; - } - } - - return null; + return WiredSourceUtil.resolveItems(ctx, source, items).stream() + .filter(item -> item != null && ctx.room().getHabboItem(item.getId()) != null) + .collect(Collectors.toList()); } private List parseItems(String data, Room room) throws WiredSaveException { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index f9121ce5..99a14b67 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -33,7 +33,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.SEND_SIGNAL; - private static final int MAX_SIGNAL_DEPTH = 10; + public static int MAX_SIGNAL_DEPTH = 100; private static final int ANTENNA_PICKED = 0; private static final int ANTENNA_TRIGGER = 1; @@ -166,7 +166,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .signalChannel(signalChannel) .signalUserCount(signalUserCount) .signalFurniCount(sourceItem != null ? 1 : 0) - .contextVariableScope(ctx.contextVariables()) + .contextVariableScope(ctx.contextVariables().copy()) .triggeredByEffect(true); if (actor != null) builder.actor(actor); @@ -286,15 +286,6 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } } - if (room != null && room.getRoomSpecialTypes() != null) { - for (HabboItem receiver : newItems) { - int count = room.getRoomSpecialTypes().countSendersTargetingReceiver(receiver.getId(), this); - if (count >= RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER) { - throw new WiredSaveException("Maximum of " + RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER + " senders per receiver reached"); - } - } - } - int delay = settings.getDelay(); if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { throw new WiredSaveException("Delay too long"); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java index 504abe1c..89aa8f0e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java @@ -170,13 +170,15 @@ public class WiredExtraTextInputVariable extends InteractionWiredExtra { } public Integer resolveCapturedValue(Room room, String rawValue) { - String normalizedValue = rawValue != null ? rawValue.trim() : ""; - if (normalizedValue.isEmpty()) { - return null; - } + String capturedValue = rawValue != null ? rawValue : ""; + String normalizedValue = capturedValue.trim(); if (this.getDisplayType(room) == DISPLAY_TEXTUAL) { - return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, normalizedValue); + return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, capturedValue); + } + + if (normalizedValue.isEmpty()) { + return null; } try { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java index 96149a5d..2dda3c39 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java @@ -22,6 +22,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { public static final int CODE = 79; public static final int MAX_MAPPING_LENGTH = 1000; public static final int MAX_MAPPING_LINES = 30; + private static final String PRESERVED_SPACE = "\u00A0"; private String mappingsText = ""; private LinkedHashMap mappings = new LinkedHashMap<>(); @@ -123,8 +124,12 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { return ""; } - String mappedValue = this.mappings.get(value); - return mappedValue != null ? mappedValue : String.valueOf(value); + if (this.mappings.containsKey(value)) { + String mappedValue = this.mappings.get(value); + return mappedValue != null ? preserveSpaces(mappedValue) : ""; + } + + return String.valueOf(value); } public Integer resolveValue(String text) { @@ -132,17 +137,16 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { return null; } - String normalizedText = text.trim(); - if (normalizedText.isEmpty()) { - return null; - } + String normalizedText = normalizePreservedSpaces(text); for (Map.Entry entry : this.mappings.entrySet()) { if (entry == null || entry.getKey() == null || entry.getValue() == null) { continue; } - if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) { + String normalizedMappingValue = normalizePreservedSpaces(entry.getValue()); + + if (normalizedMappingValue.equalsIgnoreCase(normalizedText)) { return entry.getKey(); } } @@ -195,8 +199,8 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { continue; } - String line = rawLine.trim(); - if (line.isEmpty()) { + String line = rawLine; + if (line.trim().isEmpty()) { continue; } @@ -210,7 +214,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { } String keyPart = line.substring(0, separatorIndex).trim(); - String valuePart = line.substring(separatorIndex + 1).trim(); + String valuePart = line.substring(separatorIndex + 1); try { result.put(Integer.parseInt(keyPart), valuePart); @@ -221,6 +225,14 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { return result; } + private static String preserveSpaces(String value) { + return value.replace(" ", PRESERVED_SPACE); + } + + private static String normalizePreservedSpaces(String value) { + return value.replace(PRESERVED_SPACE, " "); + } + static class JsonData { String mappingsText; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java index b77a2c1c..24029d04 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java @@ -95,6 +95,11 @@ public class WiredEffectFurniAltitude extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java index 7abbc94a..2296c9a5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java @@ -100,6 +100,11 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java index 02c2630d..dd4f73c4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java @@ -155,6 +155,11 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java index 8ee8a944..8e175a25 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java @@ -38,6 +38,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { private static final int MAX_PICKED_FURNI = 20; private static final int MAX_TILE_OFFSETS = 64; + private static final int GRID_RANGE = 4; private int sourceType = SOURCE_USER_TRIGGER; private boolean filterExisting = false; @@ -69,8 +70,20 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { int totalRaw = 0; int wiredSkipped = 0; Set result = new LinkedHashSet<>(); + Set neighborhoodItems = new LinkedHashSet<>(); for (int[] src : sourcePositions) { LOGGER.info("[FurniNeighborhood] Source: ({},{}), offsets: {}", src[0], src[1], tileOffsets.size()); + for (int[] offset : getFullGridOffsets()) { + int tx = src[0] + (offset[0] - this.targetOffsetX); + int ty = src[1] + (offset[1] - this.targetOffsetY); + + for (HabboItem item : room.getItemsAt(tx, ty)) { + if (item != null && (includeWiredItems || !(item instanceof InteractionWired))) { + neighborhoodItems.add(item); + } + } + } + for (int[] offset : tileOffsets) { int tx = src[0] + (offset[0] - this.targetOffsetX); int ty = src[1] + (offset[1] - this.targetOffsetY); @@ -91,7 +104,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { } LOGGER.info("[FurniNeighborhood] Raw={}, wiredSkipped={}, kept={}", totalRaw, wiredSkipped, result.size()); - result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), filterExisting, invert); + result = this.applyNeighborhoodModifiers(result, neighborhoodItems, ctx.targets().items()); // Always set the selector result — even if empty. // An empty result means no items matched the neighborhood, so downstream @@ -100,15 +113,51 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { LOGGER.info("[FurniNeighborhood] Set {} items as targets", result.size()); } + private List getFullGridOffsets() { + List offsets = new ArrayList<>(); + + for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) { + for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) { + offsets.add(new int[]{ x, y }); + } + } + + return offsets; + } + + private LinkedHashSet applyNeighborhoodModifiers(Set matchedTargets, + Set neighborhoodTargets, + Collection existingTargets) { + LinkedHashSet matched = new LinkedHashSet<>(matchedTargets); + + if (this.invert) { + LinkedHashSet base = new LinkedHashSet<>(neighborhoodTargets); + base.removeAll(matched); + + if (this.filterExisting) { + base.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return base; + } + + if (this.filterExisting) { + matched.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return matched; + } + private List resolveSourcePositions(WiredContext ctx, Room room) { switch (sourceType) { case SOURCE_USER_TRIGGER: { - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + Optional actor = ctx.actor(); + if (actor.isPresent()) { + return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() }); } - return ctx.actor() - .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + return ctx.tile() + .map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y })) .orElse(Collections.emptyList()); } case SOURCE_USER_SIGNAL: { @@ -260,6 +309,16 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { return true; } + @Override + public boolean hasRequiredSelectorTargets(WiredContext ctx) { + return ctx != null && ctx.targets().hasItems(); + } + + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java index 0e3c9e01..1d65e926 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java @@ -124,6 +124,11 @@ public class WiredEffectFurniOnFurni extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java index 26c16426..df012d6a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java @@ -86,6 +86,11 @@ public class WiredEffectFurniPicks extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java index 1ba14216..00f2c635 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java @@ -77,6 +77,11 @@ public class WiredEffectFurniSignal extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java index 0763cf9e..fe3e89db 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java @@ -86,6 +86,11 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java index 3f88d99a..7f5edf79 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java @@ -92,6 +92,11 @@ public class WiredEffectUsersByAction extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java index a8dc2ef8..cd0b1b25 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java @@ -90,6 +90,11 @@ public class WiredEffectUsersByName extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java index b9280099..9b73aa37 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java @@ -76,6 +76,11 @@ public class WiredEffectUsersByType extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java index f74de64a..cc78d186 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java @@ -90,6 +90,11 @@ public class WiredEffectUsersGroup extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java index e34ef8e7..c40c921e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java @@ -73,6 +73,11 @@ public class WiredEffectUsersHandItem extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java index db19bea6..454d12d2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java @@ -38,6 +38,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { private static final int MAX_PICKED_FURNI = 20; private static final int MAX_TILE_OFFSETS = 64; + private static final int GRID_RANGE = 4; private int sourceType = SOURCE_USER_TRIGGER; private boolean filterExisting = false; @@ -87,11 +88,25 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { LOGGER.debug("[Neighborhood] Target tiles: {}", targetTiles); + Set neighborhoodTiles = new HashSet<>(); + for (int[] src : sourcePositions) { + for (int[] offset : getFullGridOffsets()) { + int tx = src[0] + (offset[0] - this.targetOffsetX); + int ty = src[1] + (offset[1] - this.targetOffsetY); + neighborhoodTiles.add(tx + "," + ty); + } + } + List result = new ArrayList<>(); + List neighborhoodUsers = new ArrayList<>(); for (RoomUnit unit : room.getRoomUnits()) { String pos = unit.getX() + "," + unit.getY(); boolean onTile = targetTiles.contains(pos); + if (neighborhoodTiles.contains(pos)) { + neighborhoodUsers.add(unit); + } + LOGGER.debug("[Neighborhood] Unit id={} type={} pos={} onTile={}", unit.getId(), unit.getRoomUnitType(), pos, onTile); if (onTile) { @@ -99,7 +114,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { } } - result = new ArrayList<>(this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), filterExisting, invert)); + result = new ArrayList<>(this.applyNeighborhoodModifiers(result, neighborhoodUsers, ctx.targets().users())); LOGGER.debug("[Neighborhood] Result: {} users selected", result.size()); @@ -110,15 +125,51 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { ctx.targets().setUsers(result); } + private List getFullGridOffsets() { + List offsets = new ArrayList<>(); + + for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) { + for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) { + offsets.add(new int[]{ x, y }); + } + } + + return offsets; + } + + private LinkedHashSet applyNeighborhoodModifiers(Collection matchedTargets, + Collection neighborhoodTargets, + Collection existingTargets) { + LinkedHashSet matched = new LinkedHashSet<>(matchedTargets); + + if (this.invert) { + LinkedHashSet base = new LinkedHashSet<>(neighborhoodTargets); + base.removeAll(matched); + + if (this.filterExisting) { + base.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return base; + } + + if (this.filterExisting) { + matched.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return matched; + } + private List resolveSourcePositions(WiredContext ctx, Room room) { switch (sourceType) { case SOURCE_USER_TRIGGER: { - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + Optional actor = ctx.actor(); + if (actor.isPresent()) { + return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() }); } - return ctx.actor() - .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + return ctx.tile() + .map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y })) .orElse(Collections.emptyList()); } case SOURCE_USER_SIGNAL: { @@ -262,6 +313,16 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { return true; } + @Override + public boolean hasRequiredSelectorTargets(WiredContext ctx) { + return ctx != null && ctx.targets().hasUsers(); + } + + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java index 833d10a4..a6b954ba 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java @@ -111,6 +111,11 @@ public class WiredEffectUsersOnFurni extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java index 1ce2eaba..517f0116 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java @@ -71,6 +71,11 @@ public class WiredEffectUsersSignal extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java index 8b16f73f..7e03100e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java @@ -76,6 +76,11 @@ public class WiredEffectUsersTeam extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java index 11916849..b28ea944 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java @@ -187,6 +187,11 @@ public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEf return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { this.refreshReferenceItems(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java index 5367e136..3ab28e85 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java @@ -370,8 +370,14 @@ public class PetManager { } else { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { LOGGER.error("Missing petdata for type {}. Adding this to the database...", type); - try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type) VALUES (?)")) { + try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type, pet_name, offspring_type, happy_actions, tired_actions, random_actions, can_swim) VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement.setInt(1, type); + statement.setString(2, getFallbackPetName(type)); + statement.setInt(3, getFallbackOffspringType(type)); + statement.setString(4, ""); + statement.setString(5, ""); + statement.setString(6, ""); + statement.setString(7, "0"); statement.execute(); } @@ -411,6 +417,42 @@ public class PetManager { return this.petData.values(); } + private static String getFallbackPetName(int type) { + switch (type) { + case 0: + return "Dog"; + case 1: + return "Cat"; + case 2: + return "Crocodile"; + case 3: + return "Terrier"; + case 4: + return "Bear"; + case 5: + return "Pig"; + default: + return "pet_type_" + type; + } + } + + private static int getFallbackOffspringType(int type) { + switch (type) { + case 0: + return 29; + case 1: + return 28; + case 3: + return 25; + case 4: + return 24; + case 5: + return 30; + default: + return -1; + } + } + public Pet createPet(Item item, String name, String race, String color, GameClient client) { int type = Integer.parseInt(item.getName().toLowerCase().replace("a0 pet", "")); @@ -540,4 +582,4 @@ public class PetManager { return false; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java index b6f83d43..6ddc1796 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.core.DatabaseLoggable; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ISerialize; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.Incoming; @@ -204,23 +205,14 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable { message.appendInt(this.getMessage().length()); // Custom prefix data - String prefixText = ""; - String prefixColor = ""; - String prefixIcon = ""; - String prefixEffect = ""; - if (this.habbo != null && this.habbo.getInventory() != null && this.habbo.getInventory().getPrefixesComponent() != null) { - com.eu.habbo.habbohotel.users.UserPrefix activePrefix = this.habbo.getInventory().getPrefixesComponent().getActivePrefix(); - if (activePrefix != null) { - prefixText = activePrefix.getText(); - prefixColor = activePrefix.getColor(); - prefixIcon = activePrefix.getIcon(); - prefixEffect = activePrefix.getEffect(); - } - } - message.appendString(prefixText); - message.appendString(prefixColor); - message.appendString(prefixIcon); - message.appendString(prefixEffect); + UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.empty(); + message.appendString(customizationData.prefixText); + message.appendString(customizationData.prefixColor); + message.appendString(customizationData.prefixIcon); + message.appendString(customizationData.prefixEffect); + message.appendString(customizationData.prefixFont); + message.appendString(customizationData.nickIcon); + message.appendString(customizationData.displayOrder); } catch (Exception e) { LOGGER.error("Caught exception", e); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java index dcbdfd11..395e7a0f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java @@ -343,18 +343,16 @@ public class RoomSpecialTypes { * Adds a wired trigger to the room. * @param trigger The trigger to add */ - public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 25; - public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 5; - public static final int MAX_SENDERS_PER_RECEIVER = 5; + public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 0; + public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 0; + public static final int MAX_SENDERS_PER_RECEIVER = 0; public boolean isSignalSenderLimitReached() { - Set existing = this.getSignalSenders(); - return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM; + return false; } public boolean isSignalReceiverLimitReached() { - Set existing = this.wiredTriggers.get(WiredTriggerType.RECEIVE_SIGNAL); - return existing != null && existing.size() >= MAX_SIGNAL_RECEIVERS_PER_ROOM; + return false; } public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/translations/GoogleTranslateManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/translations/GoogleTranslateManager.java new file mode 100644 index 00000000..3e60dc90 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/translations/GoogleTranslateManager.java @@ -0,0 +1,469 @@ +package com.eu.habbo.habbohotel.translations; + +import com.eu.habbo.Emulator; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.HttpsURLConnection; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class GoogleTranslateManager { + private static final Logger LOGGER = LoggerFactory.getLogger(GoogleTranslateManager.class); + private static final int DEFAULT_TIMEOUT_MS = 5000; + private static final long CACHE_TTL_MS = 1000L * 60L * 60L * 6L; + private static final int MAX_TRANSLATION_CACHE_SIZE = 2048; + private static final int MAX_LANGUAGE_CACHE_SIZE = 32; + private static final String FREE_TRANSLATE_ENDPOINT = "https://translate.googleapis.com/translate_a/single"; + private static final List FREE_SUPPORTED_LANGUAGES = buildFreeSupportedLanguages(); + + private final Map translationCache = Collections.synchronizedMap( + new LinkedHashMap(128, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return this.size() > MAX_TRANSLATION_CACHE_SIZE; + } + }); + private final Map languagesCache = Collections.synchronizedMap( + new LinkedHashMap(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return this.size() > MAX_LANGUAGE_CACHE_SIZE; + } + }); + + public SupportedLanguagesResponse getSupportedLanguages(String displayLanguage) { + String normalizedDisplayLanguage = normalizeLanguageCode(displayLanguage, "en"); + + CachedLanguages cachedLanguages = this.languagesCache.get(normalizedDisplayLanguage); + + if ((cachedLanguages != null) && !cachedLanguages.isExpired()) { + return SupportedLanguagesResponse.success(new ArrayList<>(cachedLanguages.languages)); + } + + ArrayList supportedLanguages = new ArrayList<>(FREE_SUPPORTED_LANGUAGES); + this.languagesCache.put(normalizedDisplayLanguage, new CachedLanguages(supportedLanguages)); + return SupportedLanguagesResponse.success(supportedLanguages); + } + + public TranslationResponse translate(String text, String targetLanguage) { + String safeText = text == null ? "" : text; + String normalizedTargetLanguage = normalizeLanguageCode(targetLanguage, "en"); + + if (safeText.trim().isEmpty()) { + return TranslationResponse.success(safeText, safeText, "", normalizedTargetLanguage); + } + + String cacheKey = normalizedTargetLanguage + '\u0000' + safeText; + CachedTranslation cachedTranslation = this.translationCache.get(cacheKey); + + if ((cachedTranslation != null) && !cachedTranslation.isExpired()) { + return cachedTranslation.response; + } + + try { + String requestUrl = FREE_TRANSLATE_ENDPOINT + + "?client=gtx" + + "&sl=auto" + + "&tl=" + encode(normalizedTargetLanguage) + + "&dt=t" + + "&q=" + encode(safeText); + HttpsURLConnection connection = this.openGet(requestUrl); + + int statusCode = connection.getResponseCode(); + + if (statusCode != 200) { + return TranslationResponse.failure(safeText, normalizedTargetLanguage, this.readErrorMessage(connection)); + } + + JsonArray response = this.readJsonArray(connection.getInputStream()); + JsonArray translatedParts = response.size() > 0 && response.get(0).isJsonArray() + ? response.get(0).getAsJsonArray() + : new JsonArray(); + StringBuilder translatedText = new StringBuilder(); + + for (int index = 0; index < translatedParts.size(); index++) { + if (!translatedParts.get(index).isJsonArray()) { + continue; + } + + JsonArray translatedPart = translatedParts.get(index).getAsJsonArray(); + + if (translatedPart.size() > 0 && !translatedPart.get(0).isJsonNull()) { + translatedText.append(translatedPart.get(0).getAsString()); + } + } + + String detectedLanguage = ""; + if (response.size() > 2 && !response.get(2).isJsonNull()) { + detectedLanguage = response.get(2).getAsString(); + } + + String resolvedTranslation = translatedText.length() > 0 ? translatedText.toString() : safeText; + TranslationResponse translationResponse = TranslationResponse.success(safeText, resolvedTranslation, detectedLanguage, normalizedTargetLanguage); + + this.translationCache.put(cacheKey, new CachedTranslation(translationResponse)); + + return translationResponse; + } catch (Exception e) { + LOGGER.error("Failed to translate text with Google Translate", e); + return TranslationResponse.failure(safeText, normalizedTargetLanguage, "Failed to translate text with Google Translate."); + } + } + + public void clearCache() { + this.translationCache.clear(); + this.languagesCache.clear(); + } + + private int getTimeoutMs() { + return Math.max(1000, Emulator.getConfig().getInt("translate.google.timeout.ms", DEFAULT_TIMEOUT_MS)); + } + + private HttpsURLConnection openGet(String requestUrl) throws IOException { + HttpsURLConnection connection = (HttpsURLConnection) URI.create(requestUrl).toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(this.getTimeoutMs()); + connection.setReadTimeout(this.getTimeoutMs()); + connection.setRequestProperty("Accept", "application/json"); + return connection; + } + + private JsonObject readJson(InputStream inputStream) throws IOException { + try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + return JsonParser.parseReader(bufferedReader).getAsJsonObject(); + } + } + + private JsonArray readJsonArray(InputStream inputStream) throws IOException { + try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + return JsonParser.parseReader(bufferedReader).getAsJsonArray(); + } + } + + private String readErrorMessage(HttpsURLConnection connection) { + try { + InputStream errorStream = connection.getErrorStream(); + + if (errorStream == null) { + return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.'; + } + + try { + JsonObject errorResponse = this.readJson(errorStream); + + if (errorResponse.has("error") && errorResponse.get("error").isJsonObject()) { + JsonObject errorObject = errorResponse.getAsJsonObject("error"); + + if (errorObject.has("message")) { + return errorObject.get("message").getAsString(); + } + } + } catch (Exception ignored) { + try (InputStreamReader inputStreamReader = new InputStreamReader(errorStream, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + StringBuilder responseText = new StringBuilder(); + String line; + + while ((line = bufferedReader.readLine()) != null) { + responseText.append(line); + } + + if (responseText.length() > 0) { + return responseText.toString(); + } + } + } + } catch (Exception e) { + LOGGER.warn("Failed to parse Google Translate error response", e); + } + + try { + return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.'; + } catch (IOException e) { + return "Google Translate request failed."; + } + } + + private static String normalizeLanguageCode(String languageCode, String fallback) { + if (languageCode == null || languageCode.trim().isEmpty()) { + return fallback; + } + + String normalized = languageCode.trim().replace('_', '-'); + String[] split = normalized.split("-"); + + if (split.length <= 1) { + return normalized; + } + + return split[0] + '-' + split[1].toUpperCase(); + } + + private static String encode(String value) { + return URLEncoder.encode(value == null ? "" : value, StandardCharsets.UTF_8); + } + + private static List buildFreeSupportedLanguages() { + ArrayList languages = new ArrayList<>(); + addLanguage(languages, "af", "Afrikaans"); + addLanguage(languages, "sq", "Albanian"); + addLanguage(languages, "am", "Amharic"); + addLanguage(languages, "ar", "Arabic"); + addLanguage(languages, "hy", "Armenian"); + addLanguage(languages, "az", "Azerbaijani"); + addLanguage(languages, "eu", "Basque"); + addLanguage(languages, "be", "Belarusian"); + addLanguage(languages, "bn", "Bengali"); + addLanguage(languages, "bs", "Bosnian"); + addLanguage(languages, "bg", "Bulgarian"); + addLanguage(languages, "ca", "Catalan"); + addLanguage(languages, "ceb", "Cebuano"); + addLanguage(languages, "ny", "Chichewa"); + addLanguage(languages, "zh-CN", "Chinese (Simplified)"); + addLanguage(languages, "zh-TW", "Chinese (Traditional)"); + addLanguage(languages, "co", "Corsican"); + addLanguage(languages, "hr", "Croatian"); + addLanguage(languages, "cs", "Czech"); + addLanguage(languages, "da", "Danish"); + addLanguage(languages, "nl", "Dutch"); + addLanguage(languages, "en", "English"); + addLanguage(languages, "eo", "Esperanto"); + addLanguage(languages, "et", "Estonian"); + addLanguage(languages, "tl", "Filipino"); + addLanguage(languages, "fi", "Finnish"); + addLanguage(languages, "fr", "French"); + addLanguage(languages, "fy", "Frisian"); + addLanguage(languages, "gl", "Galician"); + addLanguage(languages, "ka", "Georgian"); + addLanguage(languages, "de", "German"); + addLanguage(languages, "el", "Greek"); + addLanguage(languages, "gu", "Gujarati"); + addLanguage(languages, "ht", "Haitian Creole"); + addLanguage(languages, "ha", "Hausa"); + addLanguage(languages, "haw", "Hawaiian"); + addLanguage(languages, "iw", "Hebrew"); + addLanguage(languages, "hi", "Hindi"); + addLanguage(languages, "hmn", "Hmong"); + addLanguage(languages, "hu", "Hungarian"); + addLanguage(languages, "is", "Icelandic"); + addLanguage(languages, "ig", "Igbo"); + addLanguage(languages, "id", "Indonesian"); + addLanguage(languages, "ga", "Irish"); + addLanguage(languages, "it", "Italian"); + addLanguage(languages, "ja", "Japanese"); + addLanguage(languages, "jw", "Javanese"); + addLanguage(languages, "kn", "Kannada"); + addLanguage(languages, "kk", "Kazakh"); + addLanguage(languages, "km", "Khmer"); + addLanguage(languages, "rw", "Kinyarwanda"); + addLanguage(languages, "ko", "Korean"); + addLanguage(languages, "ku", "Kurdish"); + addLanguage(languages, "ky", "Kyrgyz"); + addLanguage(languages, "lo", "Lao"); + addLanguage(languages, "la", "Latin"); + addLanguage(languages, "lv", "Latvian"); + addLanguage(languages, "lt", "Lithuanian"); + addLanguage(languages, "lb", "Luxembourgish"); + addLanguage(languages, "mk", "Macedonian"); + addLanguage(languages, "mg", "Malagasy"); + addLanguage(languages, "ms", "Malay"); + addLanguage(languages, "ml", "Malayalam"); + addLanguage(languages, "mt", "Maltese"); + addLanguage(languages, "mi", "Maori"); + addLanguage(languages, "mr", "Marathi"); + addLanguage(languages, "mn", "Mongolian"); + addLanguage(languages, "my", "Myanmar"); + addLanguage(languages, "ne", "Nepali"); + addLanguage(languages, "no", "Norwegian"); + addLanguage(languages, "or", "Odia"); + addLanguage(languages, "ps", "Pashto"); + addLanguage(languages, "fa", "Persian"); + addLanguage(languages, "pl", "Polish"); + addLanguage(languages, "pt", "Portuguese"); + addLanguage(languages, "pa", "Punjabi"); + addLanguage(languages, "ro", "Romanian"); + addLanguage(languages, "ru", "Russian"); + addLanguage(languages, "sm", "Samoan"); + addLanguage(languages, "gd", "Scots"); + addLanguage(languages, "sr", "Serbian"); + addLanguage(languages, "st", "Sesotho"); + addLanguage(languages, "sn", "Shona"); + addLanguage(languages, "sd", "Sindhi"); + addLanguage(languages, "si", "Sinhala"); + addLanguage(languages, "sk", "Slovak"); + addLanguage(languages, "sl", "Slovenian"); + addLanguage(languages, "so", "Somali"); + addLanguage(languages, "es", "Spanish"); + addLanguage(languages, "su", "Sundanese"); + addLanguage(languages, "sw", "Swahili"); + addLanguage(languages, "sv", "Swedish"); + addLanguage(languages, "tg", "Tajik"); + addLanguage(languages, "ta", "Tamil"); + addLanguage(languages, "tt", "Tatar"); + addLanguage(languages, "te", "Telugu"); + addLanguage(languages, "th", "Thai"); + addLanguage(languages, "tr", "Turkish"); + addLanguage(languages, "tk", "Turkmen"); + addLanguage(languages, "uk", "Ukrainian"); + addLanguage(languages, "ur", "Urdu"); + addLanguage(languages, "ug", "Uyghur"); + addLanguage(languages, "uz", "Uzbek"); + addLanguage(languages, "vi", "Vietnamese"); + addLanguage(languages, "cy", "Welsh"); + addLanguage(languages, "xh", "Xhosa"); + addLanguage(languages, "yi", "Yiddish"); + addLanguage(languages, "yo", "Yoruba"); + addLanguage(languages, "zu", "Zulu"); + languages.sort(Comparator.comparing(SupportedLanguage::getName, String.CASE_INSENSITIVE_ORDER)); + return Collections.unmodifiableList(languages); + } + + private static void addLanguage(List languages, String code, String name) { + languages.add(new SupportedLanguage(code, name)); + } + + public static class SupportedLanguage { + private final String code; + private final String name; + + public SupportedLanguage(String code, String name) { + this.code = code; + this.name = name; + } + + public String getCode() { + return this.code; + } + + public String getName() { + return this.name; + } + } + + public static class SupportedLanguagesResponse { + private final boolean success; + private final String errorMessage; + private final List languages; + + private SupportedLanguagesResponse(boolean success, String errorMessage, List languages) { + this.success = success; + this.errorMessage = errorMessage == null ? "" : errorMessage; + this.languages = languages == null ? Collections.emptyList() : languages; + } + + public static SupportedLanguagesResponse success(List languages) { + return new SupportedLanguagesResponse(true, "", languages); + } + + public static SupportedLanguagesResponse failure(String errorMessage) { + return new SupportedLanguagesResponse(false, errorMessage, Collections.emptyList()); + } + + public boolean isSuccess() { + return this.success; + } + + public String getErrorMessage() { + return this.errorMessage; + } + + public List getLanguages() { + return this.languages; + } + } + + public static class TranslationResponse { + private final boolean success; + private final String errorMessage; + private final String originalText; + private final String translatedText; + private final String detectedLanguage; + private final String targetLanguage; + + private TranslationResponse(boolean success, String errorMessage, String originalText, String translatedText, String detectedLanguage, String targetLanguage) { + this.success = success; + this.errorMessage = errorMessage == null ? "" : errorMessage; + this.originalText = originalText == null ? "" : originalText; + this.translatedText = translatedText == null ? "" : translatedText; + this.detectedLanguage = detectedLanguage == null ? "" : detectedLanguage; + this.targetLanguage = targetLanguage == null ? "" : targetLanguage; + } + + public static TranslationResponse success(String originalText, String translatedText, String detectedLanguage, String targetLanguage) { + return new TranslationResponse(true, "", originalText, translatedText, detectedLanguage, targetLanguage); + } + + public static TranslationResponse failure(String originalText, String targetLanguage, String errorMessage) { + return new TranslationResponse(false, errorMessage, originalText, originalText, "", targetLanguage); + } + + public boolean isSuccess() { + return this.success; + } + + public String getErrorMessage() { + return this.errorMessage; + } + + public String getOriginalText() { + return this.originalText; + } + + public String getTranslatedText() { + return this.translatedText; + } + + public String getDetectedLanguage() { + return this.detectedLanguage; + } + + public String getTargetLanguage() { + return this.targetLanguage; + } + } + + private static class CachedTranslation { + private final long createdAt; + private final TranslationResponse response; + + private CachedTranslation(TranslationResponse response) { + this.createdAt = System.currentTimeMillis(); + this.response = response; + } + + private boolean isExpired() { + return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS; + } + } + + private static class CachedLanguages { + private final long createdAt; + private final List languages; + + private CachedLanguages(List languages) { + this.createdAt = System.currentTimeMillis(); + this.languages = languages; + } + + private boolean isExpired() { + return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java index 6fdba07d..5e0130ff 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java @@ -23,6 +23,8 @@ public class HabboInventory { private ItemsComponent itemsComponent; private PetsComponent petsComponent; private PrefixesComponent prefixesComponent; + private NickIconsComponent nickIconsComponent; + private UserVisualSettingsComponent userVisualSettingsComponent; public HabboInventory(Habbo habbo) { this.habbo = habbo; @@ -68,6 +70,18 @@ public class HabboInventory { LOGGER.error("Caught exception", e); } + try { + this.nickIconsComponent = new NickIconsComponent(this.habbo); + } catch (Exception e) { + LOGGER.error("Caught exception", e); + } + + try { + this.userVisualSettingsComponent = new UserVisualSettingsComponent(this.habbo); + } catch (Exception e) { + LOGGER.error("Caught exception", e); + } + this.items = MarketPlace.getOwnOffers(this.habbo); } @@ -127,6 +141,22 @@ public class HabboInventory { this.prefixesComponent = prefixesComponent; } + public NickIconsComponent getNickIconsComponent() { + return this.nickIconsComponent; + } + + public void setNickIconsComponent(NickIconsComponent nickIconsComponent) { + this.nickIconsComponent = nickIconsComponent; + } + + public UserVisualSettingsComponent getUserVisualSettingsComponent() { + return this.userVisualSettingsComponent; + } + + public void setUserVisualSettingsComponent(UserVisualSettingsComponent userVisualSettingsComponent) { + this.userVisualSettingsComponent = userVisualSettingsComponent; + } + public void dispose() { this.badgesComponent.dispose(); this.botsComponent.dispose(); @@ -135,6 +165,8 @@ public class HabboInventory { this.petsComponent.dispose(); this.wardrobeComponent.dispose(); this.prefixesComponent.dispose(); + this.nickIconsComponent.dispose(); + this.userVisualSettingsComponent.dispose(); this.badgesComponent = null; this.botsComponent = null; @@ -143,6 +175,8 @@ public class HabboInventory { this.petsComponent = null; this.wardrobeComponent = null; this.prefixesComponent = null; + this.nickIconsComponent = null; + this.userVisualSettingsComponent = null; } public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserCustomizationData.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserCustomizationData.java new file mode 100644 index 00000000..371045b0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserCustomizationData.java @@ -0,0 +1,121 @@ +package com.eu.habbo.habbohotel.users; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.inventory.UserVisualSettingsComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class UserCustomizationData { + private static final Logger LOGGER = LoggerFactory.getLogger(UserCustomizationData.class); + + public final String nickIcon; + public final String displayOrder; + public final String prefixText; + public final String prefixColor; + public final String prefixIcon; + public final String prefixEffect; + public final String prefixFont; + + private UserCustomizationData(String nickIcon, String displayOrder, String prefixText, String prefixColor, String prefixIcon, String prefixEffect, String prefixFont) { + this.nickIcon = nickIcon != null ? nickIcon : ""; + this.displayOrder = UserVisualSettingsComponent.sanitizeDisplayOrder(displayOrder); + this.prefixText = prefixText != null ? prefixText : ""; + this.prefixColor = prefixColor != null ? prefixColor : ""; + this.prefixIcon = prefixIcon != null ? prefixIcon : ""; + this.prefixEffect = prefixEffect != null ? prefixEffect : ""; + this.prefixFont = prefixFont != null ? prefixFont : ""; + } + + public static UserCustomizationData fromHabbo(Habbo habbo) { + if (habbo == null) { + return empty(); + } + + String nickIcon = ""; + String displayOrder = UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER; + String prefixText = ""; + String prefixColor = ""; + String prefixIcon = ""; + String prefixEffect = ""; + String prefixFont = ""; + + if (habbo.getInventory() != null) { + if (habbo.getInventory().getNickIconsComponent() != null) { + UserNickIcon activeNickIcon = habbo.getInventory().getNickIconsComponent().getActiveNickIcon(); + + if (activeNickIcon != null && activeNickIcon.getIconKey() != null) { + nickIcon = activeNickIcon.getIconKey(); + } + } + + if (habbo.getInventory().getPrefixesComponent() != null) { + UserPrefix activePrefix = habbo.getInventory().getPrefixesComponent().getActivePrefix(); + + if (activePrefix != null) { + prefixText = activePrefix.getText(); + prefixColor = activePrefix.getColor(); + prefixIcon = activePrefix.getIcon(); + prefixEffect = activePrefix.getEffect(); + prefixFont = activePrefix.getFont(); + } + } + + if (habbo.getInventory().getUserVisualSettingsComponent() != null) { + displayOrder = habbo.getInventory().getUserVisualSettingsComponent().getDisplayOrder(); + } + } + + return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont); + } + + public static UserCustomizationData fromUserId(int userId) { + String nickIcon = ""; + String prefixText = ""; + String prefixColor = ""; + String prefixIcon = ""; + String prefixEffect = ""; + String prefixFont = ""; + String displayOrder = UserVisualSettingsComponent.loadDisplayOrder(userId); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + try (PreparedStatement nickStatement = connection.prepareStatement( + "SELECT icon_key FROM user_nick_icons WHERE user_id = ? AND active = 1 LIMIT 1")) { + nickStatement.setInt(1, userId); + + try (ResultSet set = nickStatement.executeQuery()) { + if (set.next()) { + nickIcon = set.getString("icon_key"); + } + } + } + + try (PreparedStatement prefixStatement = connection.prepareStatement( + "SELECT text, color, icon, effect, font FROM user_prefixes WHERE user_id = ? AND active = 1 LIMIT 1")) { + prefixStatement.setInt(1, userId); + + try (ResultSet set = prefixStatement.executeQuery()) { + if (set.next()) { + prefixText = set.getString("text"); + prefixColor = set.getString("color"); + prefixIcon = set.getString("icon"); + prefixEffect = set.getString("effect"); + prefixFont = set.getString("font"); + } + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while loading user customization data", e); + } + + return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont); + } + + public static UserCustomizationData empty() { + return new UserCustomizationData("", UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER, "", "", "", "", ""); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserNickIcon.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserNickIcon.java new file mode 100644 index 00000000..9b634f18 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserNickIcon.java @@ -0,0 +1,118 @@ +package com.eu.habbo.habbohotel.users; + +import com.eu.habbo.Emulator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; + +public class UserNickIcon implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(UserNickIcon.class); + + private int id; + private final int userId; + private String iconKey; + private boolean active; + private boolean needsInsert; + private boolean needsUpdate; + private boolean needsDelete; + + public UserNickIcon(ResultSet set) throws SQLException { + this.id = set.getInt("id"); + this.userId = set.getInt("user_id"); + this.iconKey = set.getString("icon_key"); + this.active = set.getBoolean("active"); + this.needsInsert = false; + this.needsUpdate = false; + this.needsDelete = false; + } + + public UserNickIcon(int userId, String iconKey) { + this.id = 0; + this.userId = userId; + this.iconKey = iconKey; + this.active = false; + this.needsInsert = true; + this.needsUpdate = false; + this.needsDelete = false; + } + + @Override + public void run() { + try { + if (this.needsInsert) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO user_nick_icons (user_id, icon_key, active) VALUES (?, ?, ?)", + Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, this.userId); + statement.setString(2, this.iconKey); + statement.setBoolean(3, this.active); + statement.execute(); + + try (ResultSet set = statement.getGeneratedKeys()) { + if (set.next()) { + this.id = set.getInt(1); + } + } + } + + this.needsInsert = false; + } else if (this.needsDelete) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "DELETE FROM user_nick_icons WHERE id = ? AND user_id = ?")) { + statement.setInt(1, this.id); + statement.setInt(2, this.userId); + statement.execute(); + } + + this.needsDelete = false; + } else if (this.needsUpdate) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE user_nick_icons SET icon_key = ?, active = ? WHERE id = ? AND user_id = ?")) { + statement.setString(1, this.iconKey); + statement.setBoolean(2, this.active); + statement.setInt(3, this.id); + statement.setInt(4, this.userId); + statement.execute(); + } + + this.needsUpdate = false; + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + public int getId() { + return this.id; + } + + public int getUserId() { + return this.userId; + } + + public String getIconKey() { + return this.iconKey; + } + + public void setIconKey(String iconKey) { + this.iconKey = iconKey; + this.needsUpdate = true; + } + + public boolean isActive() { + return this.active; + } + + public void setActive(boolean active) { + this.active = active; + this.needsUpdate = true; + } + + public void needsDelete(boolean needsDelete) { + this.needsDelete = needsDelete; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java index 6879d1be..b1b60b50 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java @@ -15,6 +15,12 @@ public class UserPrefix implements Runnable { private String color; private String icon; private String effect; + private String font; + private int catalogPrefixId; + private String displayName; + private int points; + private int pointsType; + private boolean custom; private boolean active; private boolean needsInsert; private boolean needsUpdate; @@ -29,6 +35,12 @@ public class UserPrefix implements Runnable { if (this.icon == null) this.icon = ""; this.effect = set.getString("effect"); if (this.effect == null) this.effect = ""; + this.font = readString(set, "font", ""); + this.catalogPrefixId = readInt(set, "catalog_prefix_id", 0); + this.displayName = readString(set, "display_name", this.text); + this.points = readInt(set, "points", 0); + this.pointsType = readInt(set, "points_type", 0); + this.custom = readBoolean(set, "is_custom", true); this.active = set.getBoolean("active"); this.needsInsert = false; this.needsUpdate = false; @@ -36,12 +48,22 @@ public class UserPrefix implements Runnable { } public UserPrefix(int userId, String text, String color, String icon, String effect) { + this(userId, text, color, icon, effect, "", 0, text, 0, 0, true); + } + + public UserPrefix(int userId, String text, String color, String icon, String effect, String font, int catalogPrefixId, String displayName, int points, int pointsType, boolean custom) { this.id = 0; this.userId = userId; this.text = text; this.color = color; this.icon = icon != null ? icon : ""; this.effect = effect != null ? effect : ""; + this.font = font != null ? font : ""; + this.catalogPrefixId = catalogPrefixId; + this.displayName = (displayName != null && !displayName.isEmpty()) ? displayName : text; + this.points = points; + this.pointsType = pointsType; + this.custom = custom; this.active = false; this.needsInsert = true; this.needsUpdate = false; @@ -54,14 +76,20 @@ public class UserPrefix implements Runnable { if (this.needsInsert) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "INSERT INTO user_prefixes (user_id, text, color, icon, effect, active) VALUES (?, ?, ?, ?, ?, ?)", + "INSERT INTO user_prefixes (user_id, text, color, icon, effect, font, active, catalog_prefix_id, display_name, points, points_type, is_custom) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { statement.setInt(1, this.userId); statement.setString(2, this.text); statement.setString(3, this.color); statement.setString(4, this.icon); statement.setString(5, this.effect); - statement.setBoolean(6, this.active); + statement.setString(6, this.font); + statement.setBoolean(7, this.active); + statement.setInt(8, this.catalogPrefixId); + statement.setString(9, this.displayName); + statement.setInt(10, this.points); + statement.setInt(11, this.pointsType); + statement.setBoolean(12, this.custom); statement.execute(); try (ResultSet set = statement.getGeneratedKeys()) { if (set.next()) { @@ -82,14 +110,20 @@ public class UserPrefix implements Runnable { } else if (this.needsUpdate) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, active = ? WHERE id = ? AND user_id = ?")) { + "UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, font = ?, active = ?, catalog_prefix_id = ?, display_name = ?, points = ?, points_type = ?, is_custom = ? WHERE id = ? AND user_id = ?")) { statement.setString(1, this.text); statement.setString(2, this.color); statement.setString(3, this.icon); statement.setString(4, this.effect); - statement.setBoolean(5, this.active); - statement.setInt(6, this.id); - statement.setInt(7, this.userId); + statement.setString(5, this.font); + statement.setBoolean(6, this.active); + statement.setInt(7, this.catalogPrefixId); + statement.setString(8, this.displayName); + statement.setInt(9, this.points); + statement.setInt(10, this.pointsType); + statement.setBoolean(11, this.custom); + statement.setInt(12, this.id); + statement.setInt(13, this.userId); statement.execute(); } this.needsUpdate = false; @@ -109,6 +143,13 @@ public class UserPrefix implements Runnable { public void setIcon(String icon) { this.icon = icon != null ? icon : ""; } public String getEffect() { return this.effect; } public void setEffect(String effect) { this.effect = effect != null ? effect : ""; } + public String getFont() { return this.font; } + public void setFont(String font) { this.font = font != null ? font : ""; } + public int getCatalogPrefixId() { return this.catalogPrefixId; } + public String getDisplayName() { return this.displayName; } + public int getPoints() { return this.points; } + public int getPointsType() { return this.pointsType; } + public boolean isCustom() { return this.custom; } public boolean isActive() { return this.active; } public void setActive(boolean active) { @@ -119,4 +160,29 @@ public class UserPrefix implements Runnable { public void needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; } public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; } public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; } + + private static int readInt(ResultSet set, String columnName, int defaultValue) { + try { + return set.getInt(columnName); + } catch (SQLException e) { + return defaultValue; + } + } + + private static String readString(ResultSet set, String columnName, String defaultValue) { + try { + String value = set.getString(columnName); + return value != null ? value : defaultValue; + } catch (SQLException e) { + return defaultValue; + } + } + + private static boolean readBoolean(ResultSet set, String columnName, boolean defaultValue) { + try { + return set.getBoolean(columnName); + } catch (SQLException e) { + return defaultValue; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/NickIconsComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/NickIconsComponent.java new file mode 100644 index 00000000..b3ff37cb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/NickIconsComponent.java @@ -0,0 +1,119 @@ +package com.eu.habbo.habbohotel.users.inventory; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserNickIcon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class NickIconsComponent { + private static final Logger LOGGER = LoggerFactory.getLogger(NickIconsComponent.class); + + private final List nickIcons = new ArrayList<>(); + private final Habbo habbo; + + public NickIconsComponent(Habbo habbo) { + this.habbo = habbo; + this.loadNickIcons(); + } + + private void loadNickIcons() { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT * FROM user_nick_icons WHERE user_id = ?")) { + statement.setInt(1, this.habbo.getHabboInfo().getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + this.nickIcons.add(new UserNickIcon(set)); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + public List getNickIcons() { + synchronized (this.nickIcons) { + return new ArrayList<>(this.nickIcons); + } + } + + public UserNickIcon getActiveNickIcon() { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.isActive()) { + return nickIcon; + } + } + } + + return null; + } + + public UserNickIcon getNickIcon(int id) { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.getId() == id) { + return nickIcon; + } + } + } + + return null; + } + + public UserNickIcon getNickIconByKey(String iconKey) { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.getIconKey().equalsIgnoreCase(iconKey)) { + return nickIcon; + } + } + } + + return null; + } + + public void addNickIcon(UserNickIcon nickIcon) { + synchronized (this.nickIcons) { + this.nickIcons.add(nickIcon); + } + } + + public void setActive(int nickIconId) { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + boolean shouldBeActive = (nickIcon.getId() == nickIconId); + + if (nickIcon.isActive() != shouldBeActive) { + nickIcon.setActive(shouldBeActive); + Emulator.getThreading().run(nickIcon); + } + } + } + } + + public void deactivateAll() { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.isActive()) { + nickIcon.setActive(false); + Emulator.getThreading().run(nickIcon); + } + } + } + } + + public void dispose() { + synchronized (this.nickIcons) { + this.nickIcons.clear(); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java index 28889ede..e9563d72 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java @@ -62,6 +62,15 @@ public class PrefixesComponent { return null; } + public UserPrefix getPrefixByCatalogId(int catalogPrefixId) { + synchronized (this.prefixes) { + for (UserPrefix prefix : this.prefixes) { + if (prefix.getCatalogPrefixId() == catalogPrefixId) return prefix; + } + } + return null; + } + public void addPrefix(UserPrefix prefix) { synchronized (this.prefixes) { this.prefixes.add(prefix); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/UserVisualSettingsComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/UserVisualSettingsComponent.java new file mode 100644 index 00000000..6bb15c5e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/UserVisualSettingsComponent.java @@ -0,0 +1,94 @@ +package com.eu.habbo.habbohotel.users.inventory; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class UserVisualSettingsComponent { + private static final Logger LOGGER = LoggerFactory.getLogger(UserVisualSettingsComponent.class); + public static final String DEFAULT_DISPLAY_ORDER = "icon-prefix-name"; + private static final Set ALLOWED_PARTS = new HashSet<>(Arrays.asList("icon", "prefix", "name")); + + private final Habbo habbo; + private String displayOrder = DEFAULT_DISPLAY_ORDER; + + public UserVisualSettingsComponent(Habbo habbo) { + this.habbo = habbo; + this.loadSettings(); + } + + private void loadSettings() { + this.displayOrder = loadDisplayOrder(this.habbo.getHabboInfo().getId()); + } + + public String getDisplayOrder() { + return sanitizeDisplayOrder(this.displayOrder); + } + + public void setDisplayOrder(String displayOrder) { + this.displayOrder = sanitizeDisplayOrder(displayOrder); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO user_visual_settings (user_id, display_order) VALUES (?, ?) ON DUPLICATE KEY UPDATE display_order = VALUES(display_order)")) { + statement.setInt(1, this.habbo.getHabboInfo().getId()); + statement.setString(2, this.displayOrder); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while saving user visual settings", e); + } + } + + public static String loadDisplayOrder(int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT display_order FROM user_visual_settings WHERE user_id = ? LIMIT 1")) { + statement.setInt(1, userId); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return sanitizeDisplayOrder(set.getString("display_order")); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while loading user visual settings", e); + } + + return DEFAULT_DISPLAY_ORDER; + } + + public static String sanitizeDisplayOrder(String displayOrder) { + if (displayOrder == null || displayOrder.trim().isEmpty()) { + return DEFAULT_DISPLAY_ORDER; + } + + String[] parts = displayOrder.trim().toLowerCase().split("-"); + + if (parts.length != 3) { + return DEFAULT_DISPLAY_ORDER; + } + + Set uniqueParts = new HashSet<>(); + + for (String part : parts) { + if (!ALLOWED_PARTS.contains(part) || !uniqueParts.add(part)) { + return DEFAULT_DISPLAY_ORDER; + } + } + + return String.join("-", parts); + } + + public void dispose() { + this.displayOrder = DEFAULT_DISPLAY_ORDER; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java index ed972341..4e7cefb2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java @@ -77,6 +77,22 @@ public interface IWiredEffect { return false; } + /** + * Selectors can use this to gate stack execution after their target list has + * been resolved. Returning false stops the stack before conditions/effects. + */ + default boolean hasRequiredSelectorTargets(WiredContext ctx) { + return true; + } + + /** + * Selectors that filter the current selector result should run after + * selectors that create/replace that result. + */ + default boolean usesExistingSelectorTargets() { + return false; + } + /** * Simulate this effect's execution and record intended state changes. *

diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java index 0faeee03..e88f2bf0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java @@ -112,7 +112,7 @@ public final class WiredContext { this.state = state; this.legacySettings = legacySettings; this.contextVariables = (event.getContextVariableScope() != null) - ? event.getContextVariableScope() + ? event.getContextVariableScope().copy() : new WiredContextVariableScope(); this.targets = new WiredTargets(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index 91f25afe..4301b2b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -26,6 +26,7 @@ import com.eu.habbo.habbohotel.wired.api.WiredStack; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent; import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent; import gnu.trove.map.hash.THashMap; @@ -130,6 +131,9 @@ public final class WiredEngine { /** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */ private final ConcurrentHashMap> sourceStacksByTriggerKey; + /** Track filter-selector animation tokens so rapid executions do not reset newer animations */ + private final ConcurrentHashMap filteredSelectorAnimationTokens; + /** * Create a new wired engine. * @@ -151,6 +155,7 @@ public final class WiredEngine { this.bannedRooms = new ConcurrentHashMap<>(); this.roomDiagnostics = new ConcurrentHashMap<>(); this.sourceStacksByTriggerKey = new ConcurrentHashMap<>(); + this.filteredSelectorAnimationTokens = new ConcurrentHashMap<>(); } /** @@ -426,6 +431,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); @@ -541,6 +550,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); @@ -627,6 +640,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); return !executableEffects.isEmpty(); @@ -660,6 +677,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false); if (!conditionsPassedForExecution) { return false; @@ -1011,9 +1032,27 @@ public final class WiredEngine { if (effects.isEmpty()) return Collections.emptyList(); List executedSelectors = new ArrayList<>(); + List immediateSelectors = new ArrayList<>(); + List deferredSelectors = new ArrayList<>(); for (IWiredEffect effect : effects) { if (!effect.isSelector()) continue; + + if (effect.usesExistingSelectorTargets()) { + deferredSelectors.add(effect); + } else { + immediateSelectors.add(effect); + } + } + + executeSelectorList(immediateSelectors, ctx, executedSelectors); + executeSelectorList(deferredSelectors, ctx, executedSelectors); + + return executedSelectors; + } + + private void executeSelectorList(List selectors, WiredContext ctx, List executedSelectors) { + for (IWiredEffect effect : selectors) { if (effect.requiresActor() && !ctx.hasActor()) { continue; } @@ -1022,14 +1061,17 @@ public final class WiredEngine { try { effect.execute(ctx); if (effect instanceof InteractionWiredEffect) { - executedSelectors.add((InteractionWiredEffect) effect); + InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; + executedSelectors.add(wiredEffect); + + if (wiredEffect.usesExistingSelectorTargets()) { + setFilteredSelectorState(ctx.room(), wiredEffect, "3"); + } } } catch (Exception e) { LOGGER.warn("Error executing selector: {}", e.getMessage()); } } - - return executedSelectors; } private void finalizeSelectors(List executedSelectors, WiredContext ctx, long currentTime) { @@ -1042,7 +1084,56 @@ public final class WiredEngine { for (InteractionWiredEffect wiredEffect : executedSelectors) { wiredEffect.setCooldown(currentTime); - wiredEffect.activateBox(room, actor, currentTime); + + if (wiredEffect.usesExistingSelectorTargets()) { + animateFilteredSelectorBox(room, wiredEffect); + } else { + wiredEffect.activateBox(room, actor, currentTime); + } + } + } + + private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) { + if (room == null || wiredEffect == null || room.isHideWired()) { + return; + } + + long animationToken = System.nanoTime(); + this.filteredSelectorAnimationTokens.put(wiredEffect.getId(), animationToken); + + setFilteredSelectorState(room, wiredEffect, "3", animationToken, false); + scheduleFilteredSelectorState(room, wiredEffect, "4", animationToken, 80L, false); + scheduleFilteredSelectorState(room, wiredEffect, "5", animationToken, 160L, false); + scheduleFilteredSelectorState(room, wiredEffect, "3", animationToken, 240L, true); + } + + private void scheduleFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, long delay, boolean clearToken) { + Emulator.getThreading().run(() -> setFilteredSelectorState(room, wiredEffect, state, animationToken, clearToken), delay); + } + + private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state) { + setFilteredSelectorState(room, wiredEffect, state, 0L, false); + } + + private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, boolean clearToken) { + if (room == null || wiredEffect == null || room.isHideWired()) { + return; + } + + if (animationToken != 0L) { + Long currentToken = this.filteredSelectorAnimationTokens.get(wiredEffect.getId()); + if (currentToken == null || currentToken != animationToken) { + return; + } + } + + if (!state.equals(wiredEffect.getExtradata())) { + wiredEffect.setExtradata(state); + room.sendComposer(new ItemStateComposer(wiredEffect).compose()); + } + + if (clearToken) { + this.filteredSelectorAnimationTokens.remove(wiredEffect.getId(), animationToken); } } @@ -1059,6 +1150,20 @@ public final class WiredEngine { WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx); } + private boolean selectorsHaveRequiredTargets(List executedSelectors, WiredContext ctx) { + if (executedSelectors == null || executedSelectors.isEmpty()) { + return true; + } + + for (InteractionWiredEffect selector : executedSelectors) { + if (!selector.hasRequiredSelectorTargets(ctx)) { + return false; + } + } + + return true; + } + /** * Schedule a delayed effect execution. */ diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java index 00e41a15..201461e5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java @@ -151,8 +151,20 @@ public final class WiredSourceUtil { selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems()); List selectorEffects = getOrderedSelectorEffects(originalCtx, room, triggerItem); + executeSelectorEffects(selectorCtx, selectorEffects, false); + executeSelectorEffects(selectorCtx, selectorEffects, true); + applySelectionFilterExtras(room, triggerItem, selectorCtx); + + return selectorCtx; + } + + private static void executeSelectorEffects(WiredContext selectorCtx, List selectorEffects, boolean deferred) { for (InteractionWiredEffect effect : selectorEffects) { + if (effect == null || effect.usesExistingSelectorTargets() != deferred) { + continue; + } + if (effect.requiresActor() && !selectorCtx.hasActor()) { continue; } @@ -163,10 +175,6 @@ public final class WiredSourceUtil { } catch (Exception ignored) { } } - - applySelectionFilterExtras(room, triggerItem, selectorCtx); - - return selectorCtx; } private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java index d1db42ab..4f3631b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Locale; public final class WiredTextPlaceholderUtil { + private static final char PRESERVED_SPACE = '\u00A0'; + private WiredTextPlaceholderUtil() { } @@ -87,7 +89,41 @@ public final class WiredTextPlaceholderUtil { } } - return resolvedText; + return preserveRepeatedSpaces(resolvedText); + } + + private static String preserveRepeatedSpaces(String text) { + if (text == null || text.length() < 2) { + return text; + } + + StringBuilder result = new StringBuilder(text.length()); + int index = 0; + while (index < text.length()) { + char currentChar = text.charAt(index); + if (currentChar != ' ') { + result.append(currentChar); + index++; + continue; + } + + int startIndex = index; + while (index < text.length() && text.charAt(index) == ' ') { + index++; + } + + int spaceCount = index - startIndex; + if (spaceCount == 1) { + result.append(' '); + continue; + } + + for (int spaceIndex = 0; spaceIndex < spaceCount; spaceIndex++) { + result.append(PRESERVED_SPACE); + } + } + + return result.toString(); } public static boolean requiresActor(Room room, HabboItem stackItem) { @@ -275,7 +311,7 @@ public final class WiredTextPlaceholderUtil { } String value = resolveRoomVariableValue(room, extra); - return (value == null || value.isEmpty()) ? List.of() : List.of(value); + return value == null ? List.of() : List.of(value); } private static List collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { @@ -284,7 +320,7 @@ public final class WiredTextPlaceholderUtil { } String value = resolveContextVariableValue(ctx, extra); - return (value == null || value.isEmpty()) ? List.of() : List.of(value); + return value == null ? List.of() : List.of(value); } private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java index 38764d79..3b076e0b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; public final class WiredVariableTextConnectorSupport { + private static final String PRESERVED_SPACE = "\u00A0"; + private WiredVariableTextConnectorSupport() { } @@ -71,7 +73,7 @@ public final class WiredVariableTextConnectorSupport { Map mappings = connector.getMappings(); if (mappings.containsKey(value)) { String mappedValue = mappings.get(value); - return mappedValue != null ? mappedValue : String.valueOf(value); + return mappedValue != null ? preserveSpaces(mappedValue) : ""; } } @@ -83,10 +85,7 @@ public final class WiredVariableTextConnectorSupport { return null; } - String normalizedText = text.trim(); - if (normalizedText.isEmpty()) { - return null; - } + String normalizedText = normalizePreservedSpaces(text); for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) { Integer mappedValue = connector.resolveValue(normalizedText); @@ -97,4 +96,12 @@ public final class WiredVariableTextConnectorSupport { return null; } + + private static String preserveSpaces(String value) { + return value.replace(" ", PRESERVED_SPACE); + } + + private static String normalizePreservedSpaces(String value) { + return value.replace(PRESERVED_SPACE, " "); + } } 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 a66c77e4..3297bf91 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -36,6 +36,7 @@ import com.eu.habbo.messages.incoming.helper.MySanctionStatusEvent; import com.eu.habbo.messages.incoming.helper.RequestTalentTrackEvent; import com.eu.habbo.messages.incoming.hotelview.*; import com.eu.habbo.messages.incoming.inventory.*; +import com.eu.habbo.messages.incoming.inventory.nickicons.*; import com.eu.habbo.messages.incoming.inventory.prefixes.*; import com.eu.habbo.messages.incoming.modtool.*; import com.eu.habbo.messages.incoming.navigator.*; @@ -61,6 +62,8 @@ import com.eu.habbo.messages.incoming.rooms.promotions.RequestPromotionRoomsEven import com.eu.habbo.messages.incoming.rooms.promotions.UpdateRoomPromotionEvent; import com.eu.habbo.messages.incoming.rooms.users.*; import com.eu.habbo.messages.incoming.trading.*; +import com.eu.habbo.messages.incoming.translation.TranslationLanguagesRequestEvent; +import com.eu.habbo.messages.incoming.translation.TranslationTextRequestEvent; import com.eu.habbo.messages.incoming.unknown.RequestResolutionEvent; import com.eu.habbo.messages.incoming.unknown.UnknownEvent1; import com.eu.habbo.messages.incoming.users.*; @@ -117,6 +120,7 @@ public class PacketManager { this.registerGuilds(); this.registerPets(); this.registerWired(); + this.registerTranslation(); this.registerAchievements(); this.registerFloorPlanEditor(); this.registerAmbassadors(); @@ -407,6 +411,13 @@ public class PacketManager { this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class); this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class); this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.class); + this.registerHandler(Incoming.PurchaseCatalogPrefixEvent, PurchaseCatalogPrefixEvent.class); + this.registerHandler(Incoming.SetDisplayOrderEvent, SetDisplayOrderEvent.class); + + // Nick Icons + this.registerHandler(Incoming.RequestUserNickIconsEvent, RequestUserNickIconsEvent.class); + this.registerHandler(Incoming.PurchaseNickIconEvent, PurchaseNickIconEvent.class); + this.registerHandler(Incoming.SetActiveNickIconEvent, SetActiveNickIconEvent.class); } void registerRooms() throws Exception { @@ -633,6 +644,11 @@ public class PacketManager { this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class); } + void registerTranslation() throws Exception { + this.registerHandler(Incoming.TranslationLanguagesRequestEvent, TranslationLanguagesRequestEvent.class); + this.registerHandler(Incoming.TranslationTextRequestEvent, TranslationTextRequestEvent.class); + } + void registerUnknown() throws Exception { this.registerHandler(Incoming.RequestResolutionEvent, RequestResolutionEvent.class); this.registerHandler(Incoming.RequestTalenTrackEvent, RequestTalentTrackEvent.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 cd672c70..071ba4a4 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 @@ -419,6 +419,8 @@ public class Incoming { public static final int WiredUserVariableUpdateEvent = 10025; public static final int WiredUserVariableManageEvent = 10026; public static final int WiredUserInspectMoveEvent = 10027; + public static final int TranslationLanguagesRequestEvent = 10032; + public static final int TranslationTextRequestEvent = 10033; public static final int RequestInventoryPetDelete = 10030; public static final int RequestInventoryBadgeDelete = 10031; @@ -446,4 +448,9 @@ public class Incoming { public static final int SetActivePrefixEvent = 7012; public static final int DeletePrefixEvent = 7013; public static final int PurchasePrefixEvent = 7014; + public static final int RequestUserNickIconsEvent = 7015; + public static final int PurchaseNickIconEvent = 7016; + public static final int SetActiveNickIconEvent = 7017; + public static final int PurchaseCatalogPrefixEvent = 7018; + public static final int SetDisplayOrderEvent = 7019; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/PurchaseNickIconEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/PurchaseNickIconEvent.java new file mode 100644 index 00000000..4aeaa3ca --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/PurchaseNickIconEvent.java @@ -0,0 +1,95 @@ +package com.eu.habbo.messages.incoming.inventory.nickicons; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserNickIcon; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PurchaseNickIconEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(PurchaseNickIconEvent.class); + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + + if (habbo == null) { + return; + } + + String requestedIconKey = normalizeIconKey(this.packet.readString()); + + if (requestedIconKey.isEmpty()) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid nick icon selected.")); + return; + } + + if (habbo.getInventory().getNickIconsComponent().getNickIconByKey(requestedIconKey) != null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "You already own this nick icon.")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT points, points_type, enabled FROM custom_nick_icons_catalog WHERE icon_key = ? LIMIT 1")) { + statement.setString(1, requestedIconKey); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next() || !set.getBoolean("enabled")) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "This nick icon is not available.")); + return; + } + + int points = set.getInt("points"); + int pointsType = set.getInt("points_type"); + + if (points > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < points) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); + return; + } + + if (points > 0) { + habbo.getHabboInfo().addCurrencyAmount(pointsType, -points); + this.client.sendResponse(new UserCurrencyComposer(habbo)); + } + + UserNickIcon nickIcon = new UserNickIcon(habbo.getHabboInfo().getId(), requestedIconKey); + nickIcon.run(); + habbo.getInventory().getNickIconsComponent().addNickIcon(nickIcon); + + this.client.sendResponse(new UserNickIconsComposer(habbo)); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Unable to purchase this nick icon right now.")); + } + } + + private String normalizeIconKey(String iconKey) { + if (iconKey == null) { + return ""; + } + + String normalized = iconKey.trim().toLowerCase(); + + if (normalized.endsWith(".gif")) { + normalized = normalized.substring(0, normalized.length() - 4); + } + + return normalized.matches("^[a-z0-9_-]+$") ? normalized : ""; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/RequestUserNickIconsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/RequestUserNickIconsEvent.java new file mode 100644 index 00000000..84d2344f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/RequestUserNickIconsEvent.java @@ -0,0 +1,11 @@ +package com.eu.habbo.messages.incoming.inventory.nickicons; + +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; + +public class RequestUserNickIconsEvent extends MessageHandler { + @Override + public void handle() throws Exception { + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/SetActiveNickIconEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/SetActiveNickIconEvent.java new file mode 100644 index 00000000..74717f83 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/SetActiveNickIconEvent.java @@ -0,0 +1,34 @@ +package com.eu.habbo.messages.incoming.inventory.nickicons; + +import com.eu.habbo.habbohotel.users.UserNickIcon; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer; + +public class SetActiveNickIconEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int nickIconId = this.packet.readInt(); + + if (nickIconId == 0) { + this.client.getHabbo().getInventory().getNickIconsComponent().deactivateAll(); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } + return; + } + + UserNickIcon nickIcon = this.client.getHabbo().getInventory().getNickIconsComponent().getNickIcon(nickIconId); + + if (nickIcon == null) { + return; + } + + this.client.getHabbo().getInventory().getNickIconsComponent().setActive(nickIconId); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchaseCatalogPrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchaseCatalogPrefixEvent.java new file mode 100644 index 00000000..89c07a95 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchaseCatalogPrefixEvent.java @@ -0,0 +1,84 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PurchaseCatalogPrefixEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(PurchaseCatalogPrefixEvent.class); + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + int catalogPrefixId = this.packet.readInt(); + Habbo habbo = this.client.getHabbo(); + + if (habbo == null || catalogPrefixId <= 0) { + return; + } + + if (habbo.getInventory().getPrefixesComponent().getPrefixByCatalogId(catalogPrefixId) != null) { + this.client.sendResponse(new UserNickIconsComposer(habbo)); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT display_name, text, color, icon, effect, font, points, points_type FROM custom_prefixes_catalog WHERE id = ? AND enabled = 1 LIMIT 1")) { + statement.setInt(1, catalogPrefixId); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + return; + } + + int points = set.getInt("points"); + int pointsType = set.getInt("points_type"); + + if (points > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < points) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); + return; + } + + if (points > 0) { + habbo.getHabboInfo().addCurrencyAmount(pointsType, -points); + this.client.sendResponse(new UserCurrencyComposer(habbo)); + } + + UserPrefix prefix = new UserPrefix( + habbo.getHabboInfo().getId(), + set.getString("text"), + set.getString("color"), + set.getString("icon"), + set.getString("effect"), + set.getString("font"), + catalogPrefixId, + set.getString("display_name"), + points, + pointsType, + false); + prefix.run(); + habbo.getInventory().getPrefixesComponent().addPrefix(prefix); + this.client.sendResponse(new UserNickIconsComposer(habbo)); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while purchasing catalog prefix", e); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java index 8e04000f..54fa81c8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.users.UserPrefix; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer; import com.eu.habbo.messages.outgoing.users.UserCreditsComposer; import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; @@ -19,6 +20,7 @@ import java.sql.SQLException; public class PurchasePrefixEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class); + private static final String[] ALLOWED_FONTS = { "", "pixel", "cherry", "vampiro" }; @Override public int getRatelimit() { @@ -31,6 +33,7 @@ public class PurchasePrefixEvent extends MessageHandler { String color = this.packet.readString(); String icon = this.packet.readString(); String effect = this.packet.readString(); + String font = this.packet.readString(); Habbo habbo = this.client.getHabbo(); @@ -42,6 +45,9 @@ public class PurchasePrefixEvent extends MessageHandler { int priceCredits = getSettingInt("price_credits", 5); int pricePoints = getSettingInt("price_points", 0); int pointsType = getSettingInt("points_type", 0); + int fontPriceCredits = getSettingInt("font_price_credits", 10); + int fontPricePoints = getSettingInt("font_price_points", 0); + int fontPointsType = getSettingInt("font_points_type", pointsType); // Validate text text = text.trim(); @@ -72,43 +78,67 @@ public class PurchasePrefixEvent extends MessageHandler { return; } + if (icon == null) icon = ""; + icon = icon.trim(); + + if (effect == null) effect = ""; + effect = effect.trim(); + + if (font == null) font = ""; + font = font.trim().toLowerCase(); + + if (!isAllowedFont(font)) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid font format.")); + return; + } + + int totalPriceCredits = priceCredits + (!font.isEmpty() ? fontPriceCredits : 0); + // Check credits - if (priceCredits > 0 && habbo.getHabboInfo().getCredits() < priceCredits) { + if (totalPriceCredits > 0 && habbo.getHabboInfo().getCredits() < totalPriceCredits) { this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits.")); return; } + int totalPricePointsSameType = pricePoints + ((fontPricePoints > 0 && fontPointsType == pointsType && !font.isEmpty()) ? fontPricePoints : 0); + // Check points - if (pricePoints > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < pricePoints) { + if (totalPricePointsSameType > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < totalPricePointsSameType) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); + return; + } + + if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType && habbo.getHabboInfo().getCurrencyAmount(fontPointsType) < fontPricePoints) { this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); return; } // Deduct currency - if (priceCredits > 0) { - habbo.getHabboInfo().addCredits(-priceCredits); + if (totalPriceCredits > 0) { + habbo.getHabboInfo().addCredits(-totalPriceCredits); this.client.sendResponse(new UserCreditsComposer(habbo)); } - if (pricePoints > 0) { - habbo.getHabboInfo().addCurrencyAmount(pointsType, -pricePoints); + if (totalPricePointsSameType > 0) { + habbo.getHabboInfo().addCurrencyAmount(pointsType, -totalPricePointsSameType); this.client.sendResponse(new UserCurrencyComposer(habbo)); } - // Validate icon (allow empty or known icon names) - if (icon == null) icon = ""; - icon = icon.trim(); - - // Validate effect - if (effect == null) effect = ""; - effect = effect.trim(); + if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType) { + habbo.getHabboInfo().addCurrencyAmount(fontPointsType, -fontPricePoints); + this.client.sendResponse(new UserCurrencyComposer(habbo)); + } // Create prefix - UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect); + int storedPoints = totalPricePointsSameType; + int storedPointsType = (storedPoints > 0) ? pointsType : ((!font.isEmpty() && fontPricePoints > 0) ? fontPointsType : pointsType); + + UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect, font, 0, text, storedPoints, storedPointsType, true); prefix.run(); // Insert into DB synchronously to get the ID habbo.getInventory().getPrefixesComponent().addPrefix(prefix); this.client.sendResponse(new PrefixReceivedComposer(prefix)); + this.client.sendResponse(new UserNickIconsComposer(habbo)); } private int getSettingInt(String key, int defaultValue) { @@ -142,4 +172,14 @@ public class PurchasePrefixEvent extends MessageHandler { } return false; } + + private boolean isAllowedFont(String font) { + for (String allowedFont : ALLOWED_FONTS) { + if (allowedFont.equals(font)) { + return true; + } + } + + return false; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java index 9ec5710a..16d88890 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java @@ -3,6 +3,8 @@ package com.eu.habbo.messages.incoming.inventory.prefixes; import com.eu.habbo.habbohotel.users.UserPrefix; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer; public class SetActivePrefixEvent extends MessageHandler { @Override @@ -12,6 +14,11 @@ public class SetActivePrefixEvent extends MessageHandler { if (prefixId == 0) { this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll(); this.client.sendResponse(new ActivePrefixUpdatedComposer(null)); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } return; } @@ -21,5 +28,10 @@ public class SetActivePrefixEvent extends MessageHandler { this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId); this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix)); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetDisplayOrderEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetDisplayOrderEvent.java new file mode 100644 index 00000000..b1406b15 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetDisplayOrderEvent.java @@ -0,0 +1,26 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.inventory.UserVisualSettingsComponent; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer; + +public class SetDisplayOrderEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + + if (habbo == null || habbo.getInventory() == null || habbo.getInventory().getUserVisualSettingsComponent() == null) { + return; + } + + String displayOrder = UserVisualSettingsComponent.sanitizeDisplayOrder(this.packet.readString()); + habbo.getInventory().getUserVisualSettingsComponent().setDisplayOrder(displayOrder); + this.client.sendResponse(new UserNickIconsComposer(habbo)); + + if (habbo.getHabboInfo().getCurrentRoom() != null) { + habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose()); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationLanguagesRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationLanguagesRequestEvent.java new file mode 100644 index 00000000..6ec5bb91 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationLanguagesRequestEvent.java @@ -0,0 +1,28 @@ +package com.eu.habbo.messages.incoming.translation; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.translation.TranslationLanguagesComposer; + +public class TranslationLanguagesRequestEvent extends MessageHandler { + @Override + public void handle() { + final GameClient client = this.client; + final String displayLanguage = this.packet.readString(); + + Emulator.getThreading().run(() -> { + GoogleTranslateManager.SupportedLanguagesResponse response = Emulator.getGameEnvironment() + .getGoogleTranslateManager() + .getSupportedLanguages(displayLanguage); + + client.sendResponse(new TranslationLanguagesComposer(response).compose()); + }); + } + + @Override + public int getRatelimit() { + return 250; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationTextRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationTextRequestEvent.java new file mode 100644 index 00000000..798e97ce --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationTextRequestEvent.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.incoming.translation; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.translation.TranslationResultComposer; + +public class TranslationTextRequestEvent extends MessageHandler { + @Override + public void handle() { + final GameClient client = this.client; + final int requestId = this.packet.readInt(); + final String text = this.packet.readString(); + final String targetLanguage = this.packet.readString(); + + Emulator.getThreading().run(() -> { + GoogleTranslateManager.TranslationResponse response = Emulator.getGameEnvironment() + .getGoogleTranslateManager() + .translate(text, targetLanguage); + + client.sendResponse(new TranslationResultComposer(requestId, response).compose()); + }); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java index 258e02ce..85728ba7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java @@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; import com.eu.habbo.messages.outgoing.wired.WiredSavedComposer; public class WiredEffectSaveDataEvent extends MessageHandler { @@ -39,6 +40,16 @@ public class WiredEffectSaveDataEvent extends MessageHandler { if (saved) { this.client.sendResponse(new WiredSavedComposer()); if (effect != null) { + if (effect.isSelector()) { + if (effect.usesExistingSelectorTargets()) { + effect.setExtradata("3"); + room.sendComposer(new ItemStateComposer(effect).compose()); + } else if ("3".equals(effect.getExtradata()) || "4".equals(effect.getExtradata()) || "5".equals(effect.getExtradata())) { + effect.setExtradata("0"); + room.sendComposer(new ItemStateComposer(effect).compose()); + } + } + effect.needsUpdate(true); Emulator.getThreading().run(effect); } else { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 2b01ff81..8d245894 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -124,6 +124,8 @@ public class Outgoing { public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM public final static int ConfInvisStateComposer = 5104; // CUSTOM + public final static int TranslationLanguagesComposer = 5106; // CUSTOM + public final static int TranslationResultComposer = 5107; // CUSTOM public final static int AreaHideComposer = 6001; // CUSTOM public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768 public final static int MarketplaceConfigComposer = 1823; // PRODUCTION-201611291003-338511768 @@ -576,6 +578,7 @@ public class Outgoing { public static final int UserPrefixesComposer = 7001; public static final int PrefixReceivedComposer = 7002; public static final int ActivePrefixUpdatedComposer = 7003; + public static final int UserNickIconsComposer = 7004; public static final int AvailableCommandsComposer = 4050; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/nickicons/UserNickIconsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/nickicons/UserNickIconsComposer.java new file mode 100644 index 00000000..552a4fa9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/nickicons/UserNickIconsComposer.java @@ -0,0 +1,217 @@ +package com.eu.habbo.messages.outgoing.inventory.nickicons; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; +import com.eu.habbo.habbohotel.users.UserNickIcon; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UserNickIconsComposer extends MessageComposer { + private static final Logger LOGGER = LoggerFactory.getLogger(UserNickIconsComposer.class); + + private final Habbo habbo; + + public UserNickIconsComposer(Habbo habbo) { + this.habbo = habbo; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.UserNickIconsComposer); + + if (this.habbo == null || this.habbo.getInventory() == null || this.habbo.getInventory().getNickIconsComponent() == null) { + this.response.appendInt(0); + return this.response; + } + + Map ownedByKey = new HashMap<>(); + List ownedNickIcons = this.habbo.getInventory().getNickIconsComponent().getNickIcons(); + + for (UserNickIcon nickIcon : ownedNickIcons) { + ownedByKey.put(nickIcon.getIconKey().toLowerCase(), nickIcon); + } + + Map ownedPrefixesByCatalogId = new HashMap<>(); + List ownedPrefixes = this.habbo.getInventory().getPrefixesComponent().getPrefixes(); + + for (UserPrefix prefix : ownedPrefixes) { + if (prefix.getCatalogPrefixId() > 0) { + ownedPrefixesByCatalogId.put(prefix.getCatalogPrefixId(), prefix); + } + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT icon_key, display_name, points, points_type FROM custom_nick_icons_catalog WHERE enabled = 1 ORDER BY sort_order ASC, id ASC")) { + try (ResultSet set = statement.executeQuery()) { + List catalogNickIcons = new ArrayList<>(); + + while (set.next()) { + catalogNickIcons.add(new CatalogNickIcon( + set.getString("icon_key"), + set.getString("display_name"), + set.getInt("points"), + set.getInt("points_type"))); + } + + this.response.appendInt(catalogNickIcons.size()); + + for (CatalogNickIcon catalogNickIcon : catalogNickIcons) { + UserNickIcon ownedNickIcon = ownedByKey.get(catalogNickIcon.iconKey.toLowerCase()); + + this.response.appendString(catalogNickIcon.iconKey); + this.response.appendString(catalogNickIcon.displayName != null ? catalogNickIcon.displayName : ""); + this.response.appendInt(catalogNickIcon.points); + this.response.appendInt(catalogNickIcon.pointsType); + this.response.appendInt(ownedNickIcon != null ? 1 : 0); + this.response.appendInt((ownedNickIcon != null && ownedNickIcon.isActive()) ? 1 : 0); + this.response.appendInt(ownedNickIcon != null ? ownedNickIcon.getId() : 0); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + this.response.appendInt(0); + } + + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo); + this.response.appendString(customizationData.displayOrder); + this.response.appendInt(this.getSettingInt("max_length", 15)); + this.response.appendInt(this.getSettingInt("price_credits", 5)); + this.response.appendInt(this.getSettingInt("price_points", 0)); + this.response.appendInt(this.getSettingInt("points_type", 0)); + this.response.appendInt(this.getSettingInt("font_price_credits", 10)); + this.response.appendInt(this.getSettingInt("font_price_points", 0)); + this.response.appendInt(this.getSettingInt("font_points_type", 0)); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT id, display_name, text, color, icon, effect, font, points, points_type FROM custom_prefixes_catalog WHERE enabled = 1 ORDER BY sort_order ASC, id ASC")) { + try (ResultSet set = statement.executeQuery()) { + List catalogPrefixes = new ArrayList<>(); + + while (set.next()) { + catalogPrefixes.add(new CatalogPrefix( + set.getInt("id"), + set.getString("display_name"), + set.getString("text"), + set.getString("color"), + set.getString("icon"), + set.getString("effect"), + set.getString("font"), + set.getInt("points"), + set.getInt("points_type"))); + } + + this.response.appendInt(catalogPrefixes.size()); + + for (CatalogPrefix catalogPrefix : catalogPrefixes) { + UserPrefix ownedPrefix = ownedPrefixesByCatalogId.get(catalogPrefix.id); + + this.response.appendInt(catalogPrefix.id); + this.response.appendString(catalogPrefix.displayName != null ? catalogPrefix.displayName : catalogPrefix.text); + this.response.appendString(catalogPrefix.text != null ? catalogPrefix.text : ""); + this.response.appendString(catalogPrefix.color != null ? catalogPrefix.color : ""); + this.response.appendString(catalogPrefix.icon != null ? catalogPrefix.icon : ""); + this.response.appendString(catalogPrefix.effect != null ? catalogPrefix.effect : ""); + this.response.appendString(catalogPrefix.font != null ? catalogPrefix.font : ""); + this.response.appendInt(catalogPrefix.points); + this.response.appendInt(catalogPrefix.pointsType); + this.response.appendInt(ownedPrefix != null ? 1 : 0); + this.response.appendInt((ownedPrefix != null && ownedPrefix.isActive()) ? 1 : 0); + this.response.appendInt(ownedPrefix != null ? ownedPrefix.getId() : 0); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception loading prefix catalog", e); + this.response.appendInt(0); + } + + this.response.appendInt(ownedPrefixes.size()); + + for (UserPrefix prefix : ownedPrefixes) { + this.response.appendInt(prefix.getId()); + this.response.appendString(prefix.getDisplayName() != null ? prefix.getDisplayName() : prefix.getText()); + this.response.appendString(prefix.getText()); + this.response.appendString(prefix.getColor()); + this.response.appendString(prefix.getIcon()); + this.response.appendString(prefix.getEffect()); + this.response.appendString(prefix.getFont()); + this.response.appendInt(prefix.isActive() ? 1 : 0); + this.response.appendInt(prefix.isCustom() ? 1 : 0); + this.response.appendInt(prefix.getPoints()); + this.response.appendInt(prefix.getPointsType()); + this.response.appendInt(prefix.getCatalogPrefixId()); + } + + return this.response; + } + + private int getSettingInt(String key, int defaultValue) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT `value` FROM custom_prefix_settings WHERE key_name = ? LIMIT 1")) { + statement.setString(1, key); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return Integer.parseInt(set.getString("value")); + } + } + } catch (SQLException | NumberFormatException e) { + LOGGER.error("Caught exception while resolving prefix setting {}", key, e); + } + + return defaultValue; + } + + private static class CatalogNickIcon { + private final String iconKey; + private final String displayName; + private final int points; + private final int pointsType; + + private CatalogNickIcon(String iconKey, String displayName, int points, int pointsType) { + this.iconKey = iconKey; + this.displayName = displayName; + this.points = points; + this.pointsType = pointsType; + } + } + + private static class CatalogPrefix { + private final int id; + private final String displayName; + private final String text; + private final String color; + private final String icon; + private final String effect; + private final String font; + private final int points; + private final int pointsType; + + private CatalogPrefix(int id, String displayName, String text, String color, String icon, String effect, String font, int points, int pointsType) { + this.id = id; + this.displayName = displayName; + this.text = text; + this.color = color; + this.icon = icon; + this.effect = effect; + this.font = font; + this.points = points; + this.pointsType = pointsType; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java index 13017e93..b78977e8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java @@ -22,12 +22,14 @@ public class ActivePrefixUpdatedComposer extends MessageComposer { this.response.appendString(this.prefix.getColor()); this.response.appendString(this.prefix.getIcon()); this.response.appendString(this.prefix.getEffect()); + this.response.appendString(this.prefix.getFont()); } else { this.response.appendInt(0); this.response.appendString(""); this.response.appendString(""); this.response.appendString(""); this.response.appendString(""); + this.response.appendString(""); } return this.response; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java index 98bdf055..6db2effe 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java @@ -20,6 +20,7 @@ public class PrefixReceivedComposer extends MessageComposer { this.response.appendString(this.prefix.getColor()); this.response.appendString(this.prefix.getIcon()); this.response.appendString(this.prefix.getEffect()); + this.response.appendString(this.prefix.getFont()); return this.response; } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java index 747e63b6..c75c2fe2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java @@ -30,6 +30,7 @@ public class UserPrefixesComposer extends MessageComposer { this.response.appendString(prefix.getColor()); this.response.appendString(prefix.getIcon()); this.response.appendString(prefix.getEffect()); + this.response.appendString(prefix.getFont()); this.response.appendInt(prefix.isActive() ? 1 : 0); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java index c1024c8f..e352e9d4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.outgoing.rooms.users; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -23,6 +24,14 @@ public class RoomUserDataComposer extends MessageComposer { this.response.appendInt(this.habbo.getHabboInfo().getInfostandBg()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay()); + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java index 9d634f44..fdf6857c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -66,6 +67,14 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(""); this.response.appendInt(this.habbo.getHabboStats().getAchievementScore()); this.response.appendBoolean(true); + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); this.response.appendString(this.habbo.getHabboInfo().getRoomEntryMethod()); this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId()); } else if (this.habbos != null) { @@ -99,6 +108,14 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(""); this.response.appendInt(habbo.getHabboStats().getAchievementScore()); this.response.appendBoolean(true); + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(habbo); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); this.response.appendString(habbo.getHabboInfo().getRoomEntryMethod()); this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId()); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationLanguagesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationLanguagesComposer.java new file mode 100644 index 00000000..63d9de56 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationLanguagesComposer.java @@ -0,0 +1,33 @@ +package com.eu.habbo.messages.outgoing.translation; + +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class TranslationLanguagesComposer extends MessageComposer { + private final GoogleTranslateManager.SupportedLanguagesResponse responseData; + + public TranslationLanguagesComposer(GoogleTranslateManager.SupportedLanguagesResponse responseData) { + this.responseData = responseData; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.TranslationLanguagesComposer); + this.response.appendBoolean(this.responseData != null && this.responseData.isSuccess()); + this.response.appendString(this.responseData != null ? this.responseData.getErrorMessage() : "Unknown error"); + + int count = (this.responseData != null) ? this.responseData.getLanguages().size() : 0; + this.response.appendInt(count); + + if (this.responseData != null) { + for (GoogleTranslateManager.SupportedLanguage language : this.responseData.getLanguages()) { + this.response.appendString(language.getCode()); + this.response.appendString(language.getName()); + } + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationResultComposer.java new file mode 100644 index 00000000..662e81d5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationResultComposer.java @@ -0,0 +1,29 @@ +package com.eu.habbo.messages.outgoing.translation; + +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class TranslationResultComposer extends MessageComposer { + private final int requestId; + private final GoogleTranslateManager.TranslationResponse responseData; + + public TranslationResultComposer(int requestId, GoogleTranslateManager.TranslationResponse responseData) { + this.requestId = requestId; + this.responseData = responseData; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.TranslationResultComposer); + this.response.appendInt(this.requestId); + this.response.appendBoolean(this.responseData != null && this.responseData.isSuccess()); + this.response.appendString(this.responseData != null ? this.responseData.getErrorMessage() : "Unknown error"); + this.response.appendString(this.responseData != null ? this.responseData.getOriginalText() : ""); + this.response.appendString(this.responseData != null ? this.responseData.getTranslatedText() : ""); + this.response.appendString(this.responseData != null ? this.responseData.getDetectedLanguage() : ""); + this.response.appendString(this.responseData != null ? this.responseData.getTargetLanguage() : ""); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java index 5dd3e809..e7e5d859 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.messenger.Messenger; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboInfo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -115,6 +116,14 @@ public class UserProfileComposer extends MessageComposer { this.response.appendInt(this.habboInfo.getInfostandBg()); this.response.appendInt(this.habboInfo.getInfostandStand()); this.response.appendInt(this.habboInfo.getInfostandOverlay()); + UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.fromUserId(this.habboInfo.getId()); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/Server.java b/Emulator/src/main/java/com/eu/habbo/networking/Server.java index f7a4a3ce..7ae6f43f 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/Server.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/Server.java @@ -92,4 +92,4 @@ public abstract class Server { public int getPort() { return this.port; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java index 5712a3b8..ee174242 100644 --- a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java +++ b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java @@ -13,6 +13,7 @@ import com.eu.habbo.habbohotel.games.tag.TagGame; import com.eu.habbo.habbohotel.items.ItemManager; import com.eu.habbo.habbohotel.items.interactions.InteractionPostIt; import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; +import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal; import com.eu.habbo.habbohotel.items.interactions.games.football.InteractionFootballGate; import com.eu.habbo.habbohotel.messenger.Messenger; import com.eu.habbo.habbohotel.modtool.WordFilter; @@ -116,6 +117,7 @@ public class PluginManager { RoomManager.HOME_ROOM_ID = Emulator.getConfig().getInt("hotel.home.room"); WiredManager.MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); WiredManager.TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500); + WiredEffectSendSignal.MAX_SIGNAL_DEPTH = Emulator.getConfig().getInt("wired.signal.max.depth", 100); WiredEngine.MAX_RECURSION_DEPTH = Emulator.getConfig().getInt("wired.abuse.max.recursion.depth", 10); WiredEngine.MAX_EVENTS_PER_WINDOW = Emulator.getConfig().getInt("wired.abuse.max.events.per.window", 100); WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000); From dde2c4143c8b5053e0690582025aeddc5d8ffb71 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 23 Apr 2026 07:01:09 +0200 Subject: [PATCH 3/9] checkpoint: secure config gdm and api baseline --- Emulator/pom.xml | 2 +- .../WebSocketChannelInitializer.java | 4 + .../auth/NitroSecureApiHandler.java | 223 +++++++++++ .../auth/NitroSecureAssetHandler.java | 345 ++++++++++++++++++ Latest_Compiled_Version/config.ini.example | 8 +- 5 files changed, 580 insertions(+), 2 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java create mode 100644 Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java diff --git a/Emulator/pom.xml b/Emulator/pom.xml index 46fef640..ac1ffcb3 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -6,7 +6,7 @@ com.eu.habbo Habbo - 4.1.3 + 4.1.5 UTF-8 diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java index 99a7ebbe..64c7b291 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java @@ -2,6 +2,8 @@ package com.eu.habbo.networking.gameserver; import com.eu.habbo.messages.PacketManager; import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler; +import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler; +import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler; import com.eu.habbo.networking.gameserver.codec.WebSocketCodec; import com.eu.habbo.networking.gameserver.decoders.*; import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder; @@ -50,6 +52,8 @@ public class WebSocketChannelInitializer extends ChannelInitializer> SECURE_CONTEXTS = + AttributeKey.valueOf("nitroSecureApiContexts"); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof FullHttpRequest req)) { + super.channelRead(ctx, msg); + return; + } + + String path = new QueryStringDecoder(req.uri()).path(); + + if (!path.startsWith(API_PREFIX)) { + super.channelRead(ctx, msg); + return; + } + + if (req.method() == HttpMethod.OPTIONS) { + sendCors(ctx, req); + ReferenceCountUtil.release(req); + return; + } + + if (!isSecureRequest(req)) { + super.channelRead(ctx, msg); + return; + } + + try { + String clientKey = req.headers().get("X-Nitro-Key"); + if (clientKey == null || clientKey.isBlank()) { + sendText(ctx, req, HttpResponseStatus.UNAUTHORIZED, "Missing key."); + return; + } + + SecretKey sessionKey = NitroSecureAssetHandler.deriveSessionKey(java.util.Base64.getDecoder().decode(clientKey)); + SecureApiContext secureContext = new SecureApiContext( + NitroSecureAssetHandler.getServerKeyFingerprint(), + NitroSecureAssetHandler.fingerprint(sessionKey.getEncoded()), + sessionKey + ); + + if (!req.content().isReadable()) { + enqueueContext(ctx, secureContext); + super.channelRead(ctx, msg); + return; + } + + byte[] encrypted = new byte[req.content().readableBytes()]; + req.content().getBytes(req.content().readerIndex(), encrypted); + byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8))); + + FullHttpRequest decryptedReq = new DefaultFullHttpRequest( + req.protocolVersion(), + req.method(), + req.uri(), + Unpooled.wrappedBuffer(clear) + ); + + decryptedReq.headers().setAll(req.headers()); + decryptedReq.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); + decryptedReq.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, clear.length); + + enqueueContext(ctx, secureContext); + ReferenceCountUtil.release(req); + ctx.fireChannelRead(decryptedReq); + } catch (IllegalArgumentException e) { + LOGGER.warn("Nitro secure API rejected invalid encrypted payload", e); + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, e.getMessage()); + ReferenceCountUtil.release(req); + } catch (Exception e) { + LOGGER.error("Nitro secure API failed to decrypt request", e); + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid secure payload."); + ReferenceCountUtil.release(req); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof FullHttpResponse response)) { + super.write(ctx, msg, promise); + return; + } + + SecureApiContext secureContext = pollContext(ctx); + if (secureContext == null) { + super.write(ctx, msg, promise); + return; + } + + try { + byte[] clear = readBytes(response.content()); + byte[] encrypted = NitroSecureAssetHandler.encrypt(secureContext.sessionKey(), clear); + byte[] hex = NitroSecureAssetHandler.toHex(encrypted).getBytes(StandardCharsets.UTF_8); + + FullHttpResponse encryptedResponse = new DefaultFullHttpResponse( + response.protocolVersion(), + response.status(), + Unpooled.wrappedBuffer(hex) + ); + + encryptedResponse.headers().setAll(response.headers()); + encryptedResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8"); + encryptedResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, hex.length); + encryptedResponse.headers().set("X-Nitro-Sec", "1"); + encryptedResponse.headers().set("X-Nitro-Key-Fp", secureContext.serverKeyFingerprint()); + encryptedResponse.headers().set("X-Nitro-Derive-Fp", secureContext.derivedFingerprint()); + encryptedResponse.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); + + ReferenceCountUtil.release(response); + super.write(ctx, encryptedResponse, promise); + } catch (Exception e) { + LOGGER.error("Nitro secure API failed to encrypt response", e); + super.write(ctx, msg, promise); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + Deque contexts = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (contexts != null) contexts.clear(); + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + Deque contexts = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (contexts != null) contexts.clear(); + super.exceptionCaught(ctx, cause); + } + + private static boolean isSecureRequest(FullHttpRequest req) { + return "1".equals(req.headers().get("X-Nitro-Api")); + } + + private static void enqueueContext(ChannelHandlerContext ctx, SecureApiContext context) { + Deque queue = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (queue == null) { + queue = new ArrayDeque<>(); + ctx.channel().attr(SECURE_CONTEXTS).set(queue); + } + + queue.addLast(context); + } + + private static SecureApiContext pollContext(ChannelHandlerContext ctx) { + Deque queue = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (queue == null || queue.isEmpty()) return null; + return queue.pollFirst(); + } + + private static byte[] readBytes(ByteBuf content) { + byte[] bytes = new byte[content.readableBytes()]; + content.getBytes(content.readerIndex(), bytes); + return bytes; + } + + private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT); + applyCors(req, response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text) { + byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + applyCors(req, response); + boolean keepAlive = isKeepAlive(req); + if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + var future = ctx.writeAndFlush(response); + if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + + private static void applyCors(FullHttpRequest req, FullHttpResponse response) { + String origin = req.headers().get(HttpHeaderNames.ORIGIN); + if (origin != null && !origin.isEmpty()) { + response.headers().set("Access-Control-Allow-Origin", origin); + response.headers().set("Vary", "Origin"); + response.headers().set("Access-Control-Allow-Credentials", "true"); + } + response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); + response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api"); + response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); + } + + private static boolean isKeepAlive(FullHttpRequest req) { + String connection = req.headers().get(HttpHeaderNames.CONNECTION); + return connection == null || !"close".equalsIgnoreCase(connection); + } + + private record SecureApiContext(String serverKeyFingerprint, String derivedFingerprint, SecretKey sessionKey) { + private SecureApiContext { + Objects.requireNonNull(serverKeyFingerprint); + Objects.requireNonNull(derivedFingerprint); + Objects.requireNonNull(sessionKey); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java new file mode 100644 index 00000000..297a9311 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java @@ -0,0 +1,345 @@ +package com.eu.habbo.networking.gameserver.auth; + +import com.eu.habbo.Emulator; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.*; +import io.netty.util.ReferenceCountUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.*; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { + private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureAssetHandler.class); + private static final String MASTER_KEY_CONFIG = "nitro.secure.master_key"; + private static final String BOOTSTRAP_PATH = "/nitro-sec/bootstrap"; + private static final String FILE_PATH = "/nitro-sec/file"; + private static final int MAX_BOOTSTRAP_BODY_BYTES = 4096; + private static final SecureRandom RNG = new SecureRandom(); + private static final KeyPair SERVER_KEYPAIR = createServerKeyPair(); + private static final String SERVER_KEY_FINGERPRINT = fingerprint(SERVER_KEYPAIR.getPublic().getEncoded()); + private static final Map CACHE = new ConcurrentHashMap<>(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof FullHttpRequest req)) { + super.channelRead(ctx, msg); + return; + } + + String path = new QueryStringDecoder(req.uri()).path(); + + if (!path.equals(BOOTSTRAP_PATH) && !path.equals(FILE_PATH)) { + super.channelRead(ctx, msg); + return; + } + + try { + if (req.method() == HttpMethod.OPTIONS) { + sendCors(ctx, req); + return; + } + + if (path.equals(BOOTSTRAP_PATH)) handleBootstrap(ctx, req); + else handleFile(ctx, req); + } finally { + ReferenceCountUtil.release(req); + } + } + + private void handleBootstrap(ChannelHandlerContext ctx, FullHttpRequest req) { + if (req.method() != HttpMethod.POST) { + sendText(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "Use POST.", "text/plain; charset=utf-8"); + return; + } + + if (req.content().readableBytes() > MAX_BOOTSTRAP_BODY_BYTES) { + sendText(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, "Payload too large.", "text/plain; charset=utf-8"); + return; + } + + try { + JsonObject body = JsonParser.parseString(req.content().toString(StandardCharsets.UTF_8)).getAsJsonObject(); + String clientKey = body.has("key") ? body.get("key").getAsString() : ""; + if (clientKey.isEmpty()) { + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Missing key.", "text/plain; charset=utf-8"); + return; + } + + JsonObject response = new JsonObject(); + response.addProperty("key", Base64.getEncoder().encodeToString(SERVER_KEYPAIR.getPublic().getEncoded())); + sendText(ctx, req, HttpResponseStatus.OK, response.toString(), "application/json; charset=utf-8"); + } catch (Exception e) { + LOGGER.warn("Nitro secure bootstrap failed", e); + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid bootstrap.", "text/plain; charset=utf-8"); + } + } + + private void handleFile(ChannelHandlerContext ctx, FullHttpRequest req) { + if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) { + sendText(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "Use GET.", "text/plain; charset=utf-8"); + return; + } + + QueryStringDecoder query = new QueryStringDecoder(req.uri()); + String clientKey = headerOrQuery(req, query, "X-Nitro-Key", "key"); + if (clientKey == null || clientKey.isEmpty()) { + sendText(ctx, req, HttpResponseStatus.UNAUTHORIZED, "Missing key.", "text/plain; charset=utf-8"); + return; + } + + String kind = queryParam(query, "kind"); + String file = queryParam(query, "file"); + if (!kind.equals("config") && !kind.equals("gamedata")) { + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid kind.", "text/plain; charset=utf-8"); + return; + } + + try { + SecretKey sessionKey = deriveSessionKey(Base64.getDecoder().decode(clientKey)); + byte[] clear = readAsset(kind, file); + byte[] encrypted = encrypt(sessionKey, clear); + sendText(ctx, req, HttpResponseStatus.OK, toHex(encrypted), "text/plain; charset=utf-8", true, fingerprint(sessionKey.getEncoded())); + } catch (IllegalArgumentException e) { + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, e.getMessage(), "text/plain; charset=utf-8"); + } catch (IOException e) { + sendText(ctx, req, HttpResponseStatus.NOT_FOUND, "Not found.", "text/plain; charset=utf-8"); + } catch (Exception e) { + LOGGER.error("Nitro secure asset failed kind=" + kind + " file=" + file, e); + sendText(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, "Server error.", "text/plain; charset=utf-8"); + } + } + + private static byte[] readAsset(String kind, String file) throws IOException { + String normalized = normalizeFile(file); + String rootConfigKey = kind.equals("config") ? "nitro.secure.config.root" : "nitro.secure.gamedata.root"; + String fallback = kind.equals("config") ? "Nitro-V3/public" : "Nitro-V3/public/nitro/gamedata"; + Path root = resolveRoot(rootConfigKey, fallback, kind.equals("config") + ? new String[] { "../Nitro-V3/public", "../../Nitro-V3/public", "Nitro-V3/public" } + : new String[] { "../Nitro-V3/public/nitro/gamedata", "../../Nitro-V3/public/nitro/gamedata", "Nitro-V3/public/nitro/gamedata" }); + Path target = root.resolve(normalized).normalize(); + + if (!target.startsWith(root)) throw new IllegalArgumentException("Invalid file."); + if (!Files.isRegularFile(target)) throw new IOException("Not found"); + + String cacheKey = kind + ":" + target; + long modified = Files.getLastModifiedTime(target).toMillis(); + CacheEntry cached = CACHE.get(cacheKey); + if (cached != null && cached.modified == modified) return cached.bytes; + + byte[] bytes = Files.readAllBytes(target); + if (normalized.toLowerCase().endsWith(".json")) bytes = minifyJson(bytes); + CACHE.put(cacheKey, new CacheEntry(modified, bytes)); + return bytes; + } + + private static String normalizeFile(String file) { + if (file == null) throw new IllegalArgumentException("Missing file."); + String value = URLDecoder.decode(file, StandardCharsets.UTF_8).replace('\\', '/'); + int queryIndex = value.indexOf('?'); + if (queryIndex >= 0) value = value.substring(0, queryIndex); + int fragmentIndex = value.indexOf('#'); + if (fragmentIndex >= 0) value = value.substring(0, fragmentIndex); + while (value.startsWith("/")) value = value.substring(1); + if (value.isEmpty() || value.contains("..") || value.contains(":")) throw new IllegalArgumentException("Invalid file."); + return value; + } + + private static byte[] minifyJson(byte[] bytes) { + try { + return JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).toString().getBytes(StandardCharsets.UTF_8); + } catch (Exception ignored) { + return bytes; + } + } + + private static Path resolveRoot(String configKey, String fallback, String[] alternatives) { + String configured = Emulator.getConfig().getValue(configKey, ""); + if (configured != null && !configured.isEmpty()) return Path.of(configured).toAbsolutePath().normalize(); + + for (String alternative : alternatives) { + Path path = Path.of(alternative).toAbsolutePath().normalize(); + if (Files.isDirectory(path)) return path; + } + + return Path.of(fallback).toAbsolutePath().normalize(); + } + + static SecretKey deriveSessionKey(byte[] clientPublicEncoded) throws Exception { + KeyFactory factory = KeyFactory.getInstance("EC"); + PublicKey clientPublic = factory.generatePublic(new X509EncodedKeySpec(clientPublicEncoded)); + KeyAgreement agreement = KeyAgreement.getInstance("ECDH"); + agreement.init(SERVER_KEYPAIR.getPrivate()); + agreement.doPhase(clientPublic, true); + byte[] secret = agreement.generateSecret(); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(secret); + digest.update("nitro-secure-assets-v1".getBytes(StandardCharsets.UTF_8)); + return new SecretKeySpec(digest.digest(), "AES"); + } + + static byte[] encrypt(SecretKey key, byte[] clear) throws Exception { + byte[] iv = new byte[12]; + RNG.nextBytes(iv); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); + byte[] encrypted = cipher.doFinal(clear); + byte[] out = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, out, 0, iv.length); + System.arraycopy(encrypted, 0, out, iv.length, encrypted.length); + return out; + } + + static byte[] decrypt(SecretKey key, byte[] encryptedPayload) throws Exception { + if (encryptedPayload.length < 13) throw new IllegalArgumentException("Encrypted payload is too short."); + byte[] iv = new byte[12]; + byte[] payload = new byte[encryptedPayload.length - iv.length]; + System.arraycopy(encryptedPayload, 0, iv, 0, iv.length); + System.arraycopy(encryptedPayload, iv.length, payload, 0, payload.length); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + return cipher.doFinal(payload); + } + + private static KeyPair createServerKeyPair() { + try { + String configuredSecret = Emulator.getConfig().getValue(MASTER_KEY_CONFIG, ""); + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + if (configuredSecret != null && !configuredSecret.isBlank()) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] seed = digest.digest(configuredSecret.getBytes(StandardCharsets.UTF_8)); + SecureRandom deterministic = SecureRandom.getInstance("SHA1PRNG"); + deterministic.setSeed(seed); + generator.initialize(256, deterministic); + LOGGER.info("Nitro secure assets using persistent server key from config {}", MASTER_KEY_CONFIG); + } else { + generator.initialize(256, RNG); + LOGGER.warn("Nitro secure assets using ephemeral server key because {} is empty", MASTER_KEY_CONFIG); + } + return generator.generateKeyPair(); + } catch (Exception e) { + throw new IllegalStateException("Unable to create Nitro secure server key", e); + } + } + + private static String headerOrQuery(FullHttpRequest req, QueryStringDecoder query, String header, String param) { + String value = req.headers().get(header); + return (value == null || value.isEmpty()) ? queryParam(query, param) : value; + } + + private static String queryParam(QueryStringDecoder query, String key) { + if (!query.parameters().containsKey(key) || query.parameters().get(key).isEmpty()) return ""; + return query.parameters().get(key).get(0); + } + + private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text, String contentType) { + sendBytes(ctx, req, status, text.getBytes(StandardCharsets.UTF_8), contentType, false, null); + } + + private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text, String contentType, boolean encrypted, String deriveFingerprint) { + sendBytes(ctx, req, status, text.getBytes(StandardCharsets.UTF_8), contentType, encrypted, deriveFingerprint); + } + + private static void sendBytes(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, byte[] bytes, String contentType, boolean encrypted) { + sendBytes(ctx, req, status, bytes, contentType, encrypted, null); + } + + private static void sendBytes(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, byte[] bytes, String contentType, boolean encrypted, String deriveFingerprint) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate"); + if (encrypted) response.headers().set("X-Nitro-Sec", "1"); + response.headers().set("X-Nitro-Key-Fp", SERVER_KEY_FINGERPRINT); + if (deriveFingerprint != null && !deriveFingerprint.isEmpty()) response.headers().set("X-Nitro-Derive-Fp", deriveFingerprint); + applyCors(req, response); + boolean keepAlive = isKeepAlive(req); + if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + var future = ctx.writeAndFlush(response); + if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + + private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT); + applyCors(req, response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + private static void applyCors(FullHttpRequest req, FullHttpResponse response) { + String origin = req.headers().get(HttpHeaderNames.ORIGIN); + if (origin != null && !origin.isEmpty()) { + response.headers().set("Access-Control-Allow-Origin", origin); + response.headers().set("Vary", "Origin"); + } + response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); + response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Nitro-Key"); + response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); + } + + private static boolean isKeepAlive(FullHttpRequest req) { + String connection = req.headers().get(HttpHeaderNames.CONNECTION); + return connection == null || !"close".equalsIgnoreCase(connection); + } + + static String fingerprint(byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(bytes); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 8 && i < hash.length; i++) { + builder.append(String.format("%02x", hash[i])); + } + return builder.toString(); + } catch (Exception e) { + return "unknown"; + } + } + + static String getServerKeyFingerprint() { + return SERVER_KEY_FINGERPRINT; + } + + static String toHex(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + builder.append(String.format("%02x", value & 0xff)); + } + return builder.toString(); + } + + static byte[] fromHex(String hex) { + String normalized = hex == null ? "" : hex.trim(); + if ((normalized.length() % 2) != 0) throw new IllegalArgumentException("Invalid encrypted hex payload."); + + byte[] out = new byte[normalized.length() / 2]; + for (int i = 0; i < out.length; i++) { + int high = Character.digit(normalized.charAt(i * 2), 16); + int low = Character.digit(normalized.charAt((i * 2) + 1), 16); + if (high < 0 || low < 0) throw new IllegalArgumentException("Invalid encrypted hex payload."); + out[i] = (byte) ((high << 4) | low); + } + return out; + } + + private record CacheEntry(long modified, byte[] bytes) {} +} diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example index e1eee315..98500a03 100644 --- a/Latest_Compiled_Version/config.ini.example +++ b/Latest_Compiled_Version/config.ini.example @@ -40,4 +40,10 @@ db.pool.leak_detection_ms = 20000 set to 0 to disable enc.enabled=false enc.e=3 enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b93f06d29e8870f703a216257dec7c81de0058fea4cc5116f75e6efc4e9113513e45357dc3fd43d4efab5963ef178b78bd61e81a14c603b24c8bcce0a12230b320045498edc29282ff0603bc7b7dae8fc1b05b52b2f301a9dc783b7 -enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b \ No newline at end of file +enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b + +# Nitro secure runtime assets. JSON files are read live from disk. +nitro.secure.config.root= +nitro.secure.gamedata.root= +# Set a persistent secret when using Cloudflare / multiple backend requests. +nitro.secure.master_key=change-me-to-a-long-random-secret From 585af846c4b9e39bebf8d26c76472173cd079041 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 23 Apr 2026 16:27:01 +0200 Subject: [PATCH 4/9] Add secure assets and remember login support --- Emulator/sqlupdates/remember_login_tokens.sql | 8 + .../gameserver/auth/AuthHttpHandler.java | 186 ++++++++++++++++-- .../auth/NitroSecureApiHandler.java | 57 ++++++ Latest_Compiled_Version/config.ini.example | 4 + 4 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 Emulator/sqlupdates/remember_login_tokens.sql diff --git a/Emulator/sqlupdates/remember_login_tokens.sql b/Emulator/sqlupdates/remember_login_tokens.sql new file mode 100644 index 00000000..770ec27f --- /dev/null +++ b/Emulator/sqlupdates/remember_login_tokens.sql @@ -0,0 +1,8 @@ +ALTER TABLE `users` + ADD COLUMN IF NOT EXISTS `remember_token_hash` VARCHAR(64) NOT NULL DEFAULT '' AFTER `auth_ticket`; + +ALTER TABLE `users` + ADD COLUMN IF NOT EXISTS `remember_token_expires_at` INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `remember_token_hash`; + +ALTER TABLE `users` + ADD INDEX IF NOT EXISTS `idx_users_remember_token_hash` (`remember_token_hash`); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java index b1fdbb3d..c201ded2 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java @@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.security.SecureRandom; import java.sql.*; import java.time.Instant; @@ -29,6 +30,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { private static final String REGISTER_PATH = "/api/auth/register"; private static final String FORGOT_PATH = "/api/auth/forgot-password"; private static final String LOGOUT_PATH = "/api/auth/logout"; + private static final String REMEMBER_PATH = "/api/auth/remember"; private static final String CHECK_EMAIL_PATH = "/api/auth/check-email"; private static final String CHECK_USERNAME_PATH = "/api/auth/check-username"; private static final String HEALTH_PATH = "/api/health"; @@ -49,6 +51,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { if (!path.equals(LOGIN_PATH) && !path.equals(REGISTER_PATH) && !path.equals(FORGOT_PATH) && !path.equals(LOGOUT_PATH) + && !path.equals(REMEMBER_PATH) && !path.equals(CHECK_EMAIL_PATH) && !path.equals(CHECK_USERNAME_PATH) && !path.equals(HEALTH_PATH)) { super.channelRead(ctx, msg); @@ -111,6 +114,10 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { handleLogout(ctx, req, body); return; } + if (path.equals(REMEMBER_PATH)) { + handleRemember(ctx, req, body, ip); + return; + } if (path.equals(CHECK_EMAIL_PATH)) { handleCheckEmail(ctx, req, body, ip); @@ -220,26 +227,46 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) { String ssoTicket = readString(body, "ssoTicket"); + String rememberToken = readString(body, "rememberToken"); JsonObject ok = new JsonObject(); ok.addProperty("message", "Logged out."); - if (ssoTicket == null || ssoTicket.isEmpty()) { + if ((ssoTicket == null || ssoTicket.isEmpty()) && rememberToken.isEmpty()) { sendJson(ctx, req, HttpResponseStatus.OK, ok); return; } - try (Connection conn = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement lookup = conn.prepareStatement( - "SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) { - lookup.setString(1, ssoTicket); + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) { int userId = 0; - try (ResultSet rs = lookup.executeQuery()) { - if (rs.next()) userId = rs.getInt("id"); + + if (ssoTicket != null && !ssoTicket.isEmpty()) { + try (PreparedStatement lookup = conn.prepareStatement( + "SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) { + lookup.setString(1, ssoTicket); + try (ResultSet rs = lookup.executeQuery()) { + if (rs.next()) userId = rs.getInt("id"); + } + } + } + + if (!rememberToken.isEmpty()) { + String rememberHash = sha256Hex(rememberToken); + if (userId == 0) { + try (PreparedStatement lookupRemember = conn.prepareStatement( + "SELECT id FROM users WHERE remember_token_hash = ? LIMIT 1")) { + lookupRemember.setString(1, rememberHash); + try (ResultSet rs = lookupRemember.executeQuery()) { + if (rs.next()) userId = rs.getInt("id"); + } + } + } else { + clearRememberToken(conn, rememberHash); + } } if (userId > 0) { try (PreparedStatement clear = conn.prepareStatement( - "UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) { + "UPDATE users SET auth_ticket = '', online = '0', remember_token_hash = '', remember_token_expires_at = 0 WHERE id = ? LIMIT 1")) { clear.setInt(1, userId); clear.executeUpdate(); } @@ -265,6 +292,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { String username = readString(body, "username").trim(); String password = readString(body, "password"); + boolean remember = readBoolean(body, "remember") || readBoolean(body, "rememberMe"); if (username.isEmpty() || password.isEmpty()) { sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials.")); @@ -300,12 +328,21 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { } String ssoTicket = mintSsoTicket(); + String rememberToken = ""; + int rememberExpiresAt = 0; + + if (remember && rememberEnabled()) { + rememberToken = mintRememberToken(); + rememberExpiresAt = rememberExpiresAt(); + } try (PreparedStatement upd = conn.prepareStatement( - "UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) { + "UPDATE users SET auth_ticket = ?, ip_current = ?, remember_token_hash = ?, remember_token_expires_at = ? WHERE id = ? LIMIT 1")) { upd.setString(1, ssoTicket); upd.setString(2, ip == null ? "" : ip); - upd.setInt(3, userId); + upd.setString(3, rememberToken.isEmpty() ? "" : sha256Hex(rememberToken)); + upd.setInt(4, rememberExpiresAt); + upd.setInt(5, userId); upd.executeUpdate(); } @@ -314,6 +351,10 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { JsonObject ok = new JsonObject(); ok.addProperty("ssoTicket", ssoTicket); ok.addProperty("username", rs.getString("username")); + if (!rememberToken.isEmpty()) { + ok.addProperty("rememberToken", rememberToken); + ok.addProperty("rememberExpiresAt", rememberExpiresAt); + } sendJson(ctx, req, HttpResponseStatus.OK, ok); } } catch (Exception e) { @@ -322,6 +363,69 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { } } +/* ─── Remember login ─── */ + + private void handleRemember(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { + if (!rememberEnabled()) { + sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, errorPayload("Remember login is disabled.")); + return; + } + + String rememberToken = readString(body, "rememberToken").trim(); + + if (rememberToken.isEmpty()) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing remember token.")); + return; + } + + int now = Emulator.getIntUnixTimestamp(); + String rememberHash = sha256Hex(rememberToken); + + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "SELECT id, username FROM users WHERE remember_token_hash = ? AND remember_token_expires_at > ? LIMIT 1")) { + stmt.setString(1, rememberHash); + stmt.setInt(2, now); + + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + clearRememberToken(conn, rememberHash); + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember login expired.")); + return; + } + + int userId = rs.getInt("id"); + String username = rs.getString("username"); + String ssoTicket = mintSsoTicket(); + String nextRememberToken = mintRememberToken(); + int rememberExpiresAt = rememberExpiresAt(); + + try (PreparedStatement upd = conn.prepareStatement( + "UPDATE users SET auth_ticket = ?, ip_current = ?, remember_token_hash = ?, remember_token_expires_at = ? WHERE id = ? LIMIT 1")) { + upd.setString(1, ssoTicket); + upd.setString(2, ip == null ? "" : ip); + upd.setString(3, sha256Hex(nextRememberToken)); + upd.setInt(4, rememberExpiresAt); + upd.setInt(5, userId); + upd.executeUpdate(); + } + + AuthRateLimiter.recordSuccess(ip); + + JsonObject ok = new JsonObject(); + ok.addProperty("ssoTicket", ssoTicket); + ok.addProperty("username", username); + ok.addProperty("rememberToken", nextRememberToken); + ok.addProperty("rememberExpiresAt", rememberExpiresAt); + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } + } catch (Exception e) { + LOGGER.error("Remember-login failed", e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error.")); + } + } + /* ─── Register ─── */ private void handleRegister(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { @@ -501,6 +605,12 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { return Base64.getUrlEncoder().withoutPadding().encodeToString(buf); } + private static String mintRememberToken() { + byte[] buf = new byte[48]; + RNG.nextBytes(buf); + return "remember-" + Base64.getUrlEncoder().withoutPadding().encodeToString(buf); + } + private static String readString(JsonObject obj, String key) { if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return ""; try { @@ -510,6 +620,59 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { } } + private static boolean readBoolean(JsonObject obj, String key) { + if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return false; + try { + if (obj.get(key).isJsonPrimitive()) { + String value = obj.get(key).getAsString(); + return "true".equalsIgnoreCase(value) || "1".equals(value) || "yes".equalsIgnoreCase(value); + } + } catch (Exception ignored) { + } + try { + return obj.get(key).getAsBoolean(); + } catch (Exception e) { + return false; + } + } + + private static boolean rememberEnabled() { + return Emulator.getConfig().getBoolean("login.remember.enabled", true); + } + + private static int rememberExpiresAt() { + int days = Math.max(1, Emulator.getConfig().getInt("login.remember.days", 30)); + long expiresAt = (long) Emulator.getIntUnixTimestamp() + (days * 86400L); + return (int) Math.min(Integer.MAX_VALUE, expiresAt); + } + + private static void clearRememberToken(Connection conn, String rememberHash) { + if (rememberHash == null || rememberHash.isEmpty()) return; + try (PreparedStatement clear = conn.prepareStatement( + "UPDATE users SET remember_token_hash = '', remember_token_expires_at = 0 WHERE remember_token_hash = ? LIMIT 1")) { + clear.setString(1, rememberHash); + clear.executeUpdate(); + } catch (Exception e) { + LOGGER.debug("Unable to clear remember token", e); + } + } + + private static String sha256Hex(String value) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(hash.length * 2); + + for (byte b : hash) { + builder.append(String.format("%02x", b)); + } + + return builder.toString(); + } catch (Exception e) { + throw new IllegalStateException("SHA-256 is unavailable", e); + } + } + private static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) { String ipHeader = Emulator.getConfig() != null ? Emulator.getConfig().getValue("ws.ip.header", "") @@ -565,7 +728,8 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { response.headers().set("Access-Control-Allow-Credentials", "true"); } response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); - response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With"); + response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api"); + response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); } private static boolean isKeepAlive(FullHttpRequest req) { diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java index c788988f..c29e7f90 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java @@ -1,5 +1,7 @@ package com.eu.habbo.networking.gameserver.auth; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelDuplexHandler; @@ -17,12 +19,16 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.Deque; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; public class NitroSecureApiHandler extends ChannelDuplexHandler { private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class); private static final String API_PREFIX = "/api/"; private static final AttributeKey> SECURE_CONTEXTS = AttributeKey.valueOf("nitroSecureApiContexts"); + private static final ConcurrentHashMap NONCE_CACHE = new ConcurrentHashMap<>(); + private static final long MAX_REQUEST_SKEW_MS = 90_000L; + private static final long NONCE_TTL_MS = 2 * 60 * 1000L; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { @@ -72,6 +78,7 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { byte[] encrypted = new byte[req.content().readableBytes()]; req.content().getBytes(req.content().readerIndex(), encrypted); byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8))); + clear = unwrapEnvelope(clear, req, secureContext); FullHttpRequest decryptedReq = new DefaultFullHttpRequest( req.protocolVersion(), @@ -156,6 +163,56 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { return "1".equals(req.headers().get("X-Nitro-Api")); } + private static byte[] unwrapEnvelope(byte[] clear, FullHttpRequest req, SecureApiContext secureContext) { + if (!requiresReplayEnvelope(req.method())) return clear; + + JsonObject envelope = JsonParser.parseString(new String(clear, StandardCharsets.UTF_8)).getAsJsonObject(); + long ts = envelope.has("ts") ? envelope.get("ts").getAsLong() : 0L; + String nonce = envelope.has("nonce") ? envelope.get("nonce").getAsString() : ""; + String method = envelope.has("method") ? envelope.get("method").getAsString() : ""; + String path = envelope.has("path") ? envelope.get("path").getAsString() : ""; + String body = envelope.has("body") ? envelope.get("body").getAsString() : ""; + long now = System.currentTimeMillis(); + + if (Math.abs(now - ts) > MAX_REQUEST_SKEW_MS) { + throw new IllegalArgumentException("Secure request expired."); + } + + if (!req.method().name().equalsIgnoreCase(method)) { + throw new IllegalArgumentException("Secure request method mismatch."); + } + + String requestPath = req.uri(); + if (!requestPath.equals(path)) { + throw new IllegalArgumentException("Secure request path mismatch."); + } + + if (nonce.isBlank()) { + throw new IllegalArgumentException("Missing secure request nonce."); + } + + cleanupExpiredNonces(now); + + String replayKey = secureContext.derivedFingerprint() + ':' + nonce; + if (NONCE_CACHE.putIfAbsent(replayKey, now + NONCE_TTL_MS) != null) { + throw new IllegalArgumentException("Secure request replay detected."); + } + + return java.util.Base64.getDecoder().decode(body); + } + + private static boolean requiresReplayEnvelope(HttpMethod method) { + return method == HttpMethod.POST + || method == HttpMethod.PUT + || method == HttpMethod.PATCH + || method == HttpMethod.DELETE; + } + + private static void cleanupExpiredNonces(long now) { + if (NONCE_CACHE.size() < 512) return; + NONCE_CACHE.entrySet().removeIf(entry -> entry.getValue() < now); + } + private static void enqueueContext(ChannelHandlerContext ctx, SecureApiContext context) { Deque queue = ctx.channel().attr(SECURE_CONTEXTS).get(); if (queue == null) { diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example index 98500a03..07657db0 100644 --- a/Latest_Compiled_Version/config.ini.example +++ b/Latest_Compiled_Version/config.ini.example @@ -47,3 +47,7 @@ nitro.secure.config.root= nitro.secure.gamedata.root= # Set a persistent secret when using Cloudflare / multiple backend requests. nitro.secure.master_key=change-me-to-a-long-random-secret + +# Remember-me login tokens. +login.remember.enabled=true +login.remember.days=30 From f51617d09274c8fa875e715d0e6120b75e3e6800 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Fri, 24 Apr 2026 15:55:39 +0200 Subject: [PATCH 5/9] Add secure mode config toggles --- .../gameserver/auth/NitroSecureApiHandler.java | 10 ++++++++++ .../gameserver/auth/NitroSecureAssetHandler.java | 10 ++++++++++ Latest_Compiled_Version/config.ini.example | 2 ++ 3 files changed, 22 insertions(+) diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java index c29e7f90..cfeab3bd 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java @@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; public class NitroSecureApiHandler extends ChannelDuplexHandler { private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class); + private static final String ENABLED_CONFIG = "nitro.secure.api.enabled"; private static final String API_PREFIX = "/api/"; private static final AttributeKey> SECURE_CONTEXTS = AttributeKey.valueOf("nitroSecureApiContexts"); @@ -39,6 +40,11 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { String path = new QueryStringDecoder(req.uri()).path(); + if (!secureApiEnabled()) { + super.channelRead(ctx, msg); + return; + } + if (!path.startsWith(API_PREFIX)) { super.channelRead(ctx, msg); return; @@ -163,6 +169,10 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { return "1".equals(req.headers().get("X-Nitro-Api")); } + private static boolean secureApiEnabled() { + return com.eu.habbo.Emulator.getConfig().getBoolean(ENABLED_CONFIG, true); + } + private static byte[] unwrapEnvelope(byte[] clear, FullHttpRequest req, SecureApiContext secureContext) { if (!requiresReplayEnvelope(req.method())) return clear; diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java index 297a9311..a2da7b4e 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java @@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureAssetHandler.class); private static final String MASTER_KEY_CONFIG = "nitro.secure.master_key"; + private static final String ENABLED_CONFIG = "nitro.secure.assets.enabled"; private static final String BOOTSTRAP_PATH = "/nitro-sec/bootstrap"; private static final String FILE_PATH = "/nitro-sec/file"; private static final int MAX_BOOTSTRAP_BODY_BYTES = 4096; @@ -48,6 +49,11 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { String path = new QueryStringDecoder(req.uri()).path(); + if (!secureAssetsEnabled()) { + super.channelRead(ctx, msg); + return; + } + if (!path.equals(BOOTSTRAP_PATH) && !path.equals(FILE_PATH)) { super.channelRead(ctx, msg); return; @@ -184,6 +190,10 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { return Path.of(fallback).toAbsolutePath().normalize(); } + private static boolean secureAssetsEnabled() { + return Emulator.getConfig().getBoolean(ENABLED_CONFIG, true); + } + static SecretKey deriveSessionKey(byte[] clientPublicEncoded) throws Exception { KeyFactory factory = KeyFactory.getInstance("EC"); PublicKey clientPublic = factory.generatePublic(new X509EncodedKeySpec(clientPublicEncoded)); diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example index 07657db0..a0d232d4 100644 --- a/Latest_Compiled_Version/config.ini.example +++ b/Latest_Compiled_Version/config.ini.example @@ -43,6 +43,8 @@ enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b # Nitro secure runtime assets. JSON files are read live from disk. +nitro.secure.assets.enabled=true +nitro.secure.api.enabled=true nitro.secure.config.root= nitro.secure.gamedata.root= # Set a persistent secret when using Cloudflare / multiple backend requests. From 9bad1eb3f6d445b3d00b81f2a3b21ad3c48093cb Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Sat, 25 Apr 2026 13:30:00 +0200 Subject: [PATCH 6/9] Update secure configuration example paths --- Latest_Compiled_Version/config.ini.example | 1 + 1 file changed, 1 insertion(+) diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example index a0d232d4..0c4f1e5d 100644 --- a/Latest_Compiled_Version/config.ini.example +++ b/Latest_Compiled_Version/config.ini.example @@ -45,6 +45,7 @@ enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd # Nitro secure runtime assets. JSON files are read live from disk. nitro.secure.assets.enabled=true nitro.secure.api.enabled=true +# Point this to your deployed Nitro `/configuration` folder when secure config assets are enabled. nitro.secure.config.root= nitro.secure.gamedata.root= # Set a persistent secret when using Cloudflare / multiple backend requests. From 37d7885663ca56c362b58628189fa10cd21dd6d8 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Tue, 5 May 2026 12:09:05 +0200 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=86=99=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Habbo-4.1.2-jar-with-dependencies.jar | Bin 23456963 -> 23456963 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar index c15dee6a3a3f9fee06f2a0016772aa104de3ce6d..c396897c6db9e6d75b768447cd5c00817e64e4aa 100644 GIT binary patch delta 64742 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 z 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 x9yS~Ta5k9vSu{4x#i6!4f(n9 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

0cL!lq(8Y$Zcfwk$$$)NhHm77S^)(wnT!cuG{g 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%0D8XqY<2i100*rMb2QgT z09)`qfNk1(u1d}TD5*S8+kGv7(UNJtYMuySnlAvB9=Sl9@+N?1(z**Z68-|P%j93A zW=saO`S+6}VKMl0nVAZ|ZfTBY01hEu09~CcR{O|80I#L8nmSpS3t$W+TdcMX2GBFl z0W`AF5;gK5fVD2MR1+@+&@)MvX}h!sutt{v?EBT0>-ljdfSS{+P(336EaEM|IxYOJ zPCEvmo{}rItYrYkf7(@gXefa3{~181bzZF<{GJW<*J$>mHk4nhW-J463`xIEgR=*q z6&;(cSo?408F)Uu`3Axo;@A@FHRB?HjG(a_G(6kba0@9ZaH2NBVS5NhJ>YVZHNhz;^xy;6-l# zJqlj{oU-)Wt08%t0JqfIr>3p}@Nvjg`!&qF0{Hy;O#q*eZs1iv?*_1}ya&|y=>W#~ z-vXR!@w*LXodHcT)#`pQ+Hgv>cLF#`%XLU~jkV!5fak-yhqaRX0CY{EBbsX_fS&yh zU;{@S)k7z3D058opltcRz5KH&=yFf1bKM62` Date: Tue, 5 May 2026 12:50:28 +0200 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=86=99=20Update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Habbo-4.1.2-jar-with-dependencies.jar | Bin 23456963 -> 23456963 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar index c396897c6db9e6d75b768447cd5c00817e64e4aa..c60617dec570ef877ac109508d11b01554cc0e4b 100644 GIT binary patch delta 72724 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 QB*@IA-K8GKocSTLqSolHg6^yYr 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

9VFEESGNQCqN-xAbQnj zi^B~J@yZ?k=`*>svVQ{9kpPMc5)7Tu2T#fAw~bszCO|rMQ71IQm9$3wdRBr#_pZ%J zfUR_3WXAdg=uG3=E`Z1y^a+h|9}XW%!Da{xR_;bSCTfvCB|x#i=#a{**b9{V{CWbv zD$Tpe^L7HbQ*KH%)=bH3_Y&ZvI68OuY9gE=-@E}kI-UlI+t!JYM=f6BJHXBZg>8we zWr&c*cK*aY*K&Cx?gfhOxX~bX%VAd(cA7pcDME}u$-yZR&e2iN!7u9vtVFTK+UV!g z;+bEzV^2PAhlPeG8WyJ>MJF0=`9~xq@_U%P2?LW8p^}_XT9ZV$L58GkmIz+n0wJw` zA{-+xIxX{qj<3gDGod*KJt)jF4)WPjL{bZ!9M>P12t$eUC_n4JAL0-^$(~jM_|wGy z#$`|U^U(Ce;6$h+8NC>hh|f2OS`SW6gzLoN(y57t?V8Qg6Co!?AlUqp2+v8LpAIDg zBL!hc68RA_?tbSVON2|p6SF?2F)2!=pGm}ne!|Z_?-;YQ61#sV4hbI{frl>Vk|i60 z-+M`W&LtW)i&s=8;v;yXZ1+ov@PJrqjCWMF6Qi&HNQ6(c7V-L7BK$?hS7V$6Z^-~2 zIwipxQjEE-Nw~)-h%F3A0u^m{wfxX6`T#OA5RdBO(To6kcbR$OYrZzDx`G>@EH(*_ zQqNq8OM*12+BG8y4pY0YG)jWo#OD4MNl;27*T8R*@bw%~->@!8{5d$@GW zTlP+ZI?B}!NrI2mj*QVs@P&$HPD+BlL~QHyBv`0NC`y8J;evww;Us8EdnH{jCGp7& zuj1^@B&ej(p!jYQln}p}HA#jO+qY_y3<{|H8+jmh?B6EAC=%5yd|Mh%4sc?tJ|^LG zih}(uOKfX!@JX{`ZTF;n6;-(jUOkZ}eMtf{lEF;O(C_@7F-wLqM5u{nGV~$_cgT|Y zZGI%5T@lpt0KY=s%bI28Wa5b~{;ai}otuWwB(w9CeEW5aV=~?)7udydW$iUMKt6S0 ztLtZcAKzq{O(SOctI}uduz$mi|5L>KLO%FPt-_P}i+#MEpCgjN zk$8G|XUov(I6ySRMYKN?ahh7%2w%=-UNcZ7q^{~@yl5hdyopUVoRk<7pNt1K#X@Yk zx^GSpwrCJ*`ymA%fU3HPaR3{FpU%?bq-2ODA$g`J!&IuTBy5px3>tQw2^)rtN3f_k zUSFHv`P^rqQyN&7WhTQ|661sh$%gHziO7~M<<4)CY;c>anaztR3XE-lz%LlU!&(;^Xz!C;dD}u)5%atMx(~(+wrLh zj*xLR8J-g(>U+uf$dNFP#lu|>8(h*6pUamb(^Cv+GY9T`g|5JhvoXG zqkAZMl8ky*qC5=;X2SyN1*O{#4_IuYFY!?ro_x_I#c;zj zta}Q7iIL|jb5lS|8r-9IieU>Ur+*4uqOMFDnZjS1;J7d$1>$KqOP-bjKhWf~swf3- zISYhs%Tl0*=&o3m0?kOTAFfM*5YoZG9lu)~L)RH#%i6x;g8^@^kJN2@3OpjO*%-&2 z>7+T8Kc#>-m0KFxGD3}hpreDn(X}zP+GuxHy&H zTIXavEK}hrk+rfVEdNreNHY!#6Tp+s$LOe*ZAWK>crf0Y$$-dUNJsy|V6Br*TkN2I&kLNA?iBx5JZ zw5i}uT^rFb6(Wg3k7lWSJ%{tYuVt#?KGw(9sW6su$Fxg@wxp@&+NZ)}>Wp7Hro!J8 zv6hZXg~Qaif~lznbKE>V6&etQ7qe3N>q5MW{qs{nM#(daQtw;urPp?n&2juv~67~zH};#NL{7rzX-1v zoNXmmW*zwtZ7^oq3q`PIV=Amv3X+*eQZWt^1u}zeUghBlwEil6n4`X&iVk}8iBy;- z!pAcFzm|v}{dvmXXq0Uaa}oUqFoDa)nfJoDvE*v1A#xWIDj zV=8}Yh|6jPXy7>|=bCBYTXGYh2iR9kL%uhg>;0fQPJ;`b>Px+FF7d*q7lvKgHX8oO z45#tjUIXK(>On3V!oyvQqYm)b;W zV29wS4FAtaDqVw9{jVbKL|nqeZ3qumVuvDROJu!pMo=$VgB#6)m4|5>ctc#>&D6kE z%01Lr14}6UvK!~Vw**@z6IcB4wbCRF__DDM_ zqRxNg;cgLPJ4mm_Xy686f2)0H+7k!a0&kYtmjAv6XGG4*=W1{P?jvGbOCFOo(4P!( zCT3ru@q7CZ^$)j3KZXw`{VzKvQi)Cj{lpI)NY=}-qfPm59!Rz&8opl6ljX>}y@@UV zF$Lf9La*Lvc|WpYKQzHGMlc{*ZPaijUr2CfmV z`+lAjbi4KqsoaK|U?%Rg2Qrx-H)!=i4P2yRX6PL8tO}<)^N0rCn%i-93e$9tve0T5 z_~^&V$@fv&nB;*!QXySCqk*n=qDTVnoh`2?ij02cf1R(!JjY-Fd5vOPq-@T%d7`XE z+-)Pp+|ocZVsYXf4NReeGlQBOtigh4Jv0##JnDE=? zjo19e9oTi?X($k=)z2L*A zbsCP_A{o*y4Xnvh67ltxV`M*Hx})Gs1o?!K3-%60lY5PClwIa*)Q(8Q)#RxBG?+)o zQh!VmwKhZ2a7eVkwq_rg=68-sh}%NiJshheCJtam2Voty?`MEvU1H2y3rNBBG^4diBmOhmgpXMPW(AsaRvqispOC=ITO zA7GJeR;BUF3A{ihOT&X&BI&Uq4NudEvL1bY{dF>0>w7$wpNYp3bFRBERWu(zEVre> zY^ruKMxx^^1pa|YTW>kC{7QtsF;dnqX^=>K&OVd|zmuxv9!rA~Yau+VNv;ilK;|M{ z_477uY%dJIJ6uSEVk<$?-K&0YK3y+myYM;|KD&al;=gKR%c>AnDRspOLJ?K*>`od? zF&1>g5AR8HLB=mwh@d@M+WAKsXo%nE57YP&EZ*vtHEA%By8PUSG)SR-wt{rf5vwJZ z>5xRadBY(cBx_M@l`0+f5pE|8^D(00#7cwG@!Xh59>*ElEy~>xmCjdrID#xO9WqI2 zq>95KYcZyEcNXX19jRFo|8<@#RP%s_=`fr+_(PU(2tPZSLK?6QPk<~r=44?k=1J8Nlvb~>!1zT24hqDL$W z8h_3XxesTh8jKdCg5K%Sm0EJHA2yW~C-B>Jup||E64tNgG!ph=Ss0<=i`YxkG4?7L zosM_TM60_Oro%Q8fX%FQSZpcyY1?zs!3rD?6Kq*D5>rdni%?`F({dy+fCL$_DjkBz zwRhT@&QI+KW!>?Wsqo$DFqXu$d4D>16T;bp>9{^AD!SO!d%#n4Ion+I-kdkwoJE~U z2P<~D0jf}0MLN8qK7M;T9l8^V&gawNGcnQSVmd!4#cLjk7JiSC-L9wO{YgOJRV<2L9yKPP7xOf@Y%13D6Oi*Z6wb36043trR@8#Z{0lKc~=QCL$9 zE6L5kfxZX(yuAZfc}*1JTrqISm6lL z+zfuSktY`};7QSjV^4OjU5X>oMLSlpH4R6ie+h$WyEO(AcB>aB{#)CI@5 zcG*;9KyzZW3ufTXJTJZZR0coX&65MpW*Fjn5=02rt{rqU|+s46)qt_{QRFS zJJ^kn=P?&E_&f5v^zlm>@RpJlS2OsVe>`{2%?$qf2~WD;&EQM(Jn8)~1A|+^^0ExS zx&}zTnTdWbsqMjSCFluqNYqBvWWXB=!G=B0fI*b3et|tegj@cd!6((6aR0X%aF-lh zE6~DG3R7j)(_3uE<+B#<`pLMgHUl-fxuq5^($L}Uti_Alf}wMXElNF6hK(!@u^mTd zsysfs{7a#QAgapKQ;W|8iXuT{D zaif*0y7_ox7`Ap^y2g6i$Pk5Cka@GVy#qU!mrCOyg-A%gCrDr@*;Ak z>CHXcv~VrFqgo2pTD$`-Dts2J#r-Lf?3tj&x5Gr;yV9n6^7Ze@hGniI6di8ErD$=N zKp<<=v~Y`(-dZipBGcH`NQ)Qr1&MiG10TLZ5?##nycA#wE9r1kEjW`bt-C*-yA~~N zI1MMb&m%DGTWX=4G&Xtq70*bdfoJS#W<*_M(T)pAu+go(7M|#RL|3iBzTCTOp$UQG>s{FJ99Yh5sIp@rr#DHPXRr_d|HZWAUTba2oFawe^q#-J_OD zrBVr6tyZ_jEhpZG)H!{h>#fg{*SN<*l`yo(B5&1=!^VOY-f9sec;oAt*j%XNKHcE0 zPVq_JuC3ncq`3_rx6NC5QozFP-m0#LsfGLd(nOX-_cAGs_0~H;6`a2BRw`$i{obk( zi!%N&{FIie{B!sFI&kf~aBUoQn%=3u!?~Qr+<8W~0F!z33*KrT+r`z}Xhc*g{+_pf zpB&mX+tj*3@1Ll&w)ieZJwJ>x`h%URt@Zo#MwG6-qg7U`)fswHKd}b!P zZpz&1H_-SFS!c2OoYQ+mKej5w2X~6b*tJ(2 zxj!B8Mp^sI_+naVzQXF9SIbAnkfwEf&^?&AWvYKkmml8d!)G+k@cK}Ymf6_YM}KZg zD|ot@4^CJzSP^Y%A=SPV>7xqKGQZpU;1f?KLiLV5>O0L*wKER9VBFeWee{`ZT3D4H zJ~C^%(FYa9+&T>OQ6-4;Vtp`38P6IR8QeQJ8gr4PW)^^FQ{$pl4(FBOK6?F7OUr~c zF?K`_u$1x=icZ9!vNkHeH?s+$Z&=zM~$U^$7cHIeLFhs@pmJc=wu*ez>XQ#^3Z&UTi|Owm&Qt zi=2~YG))K{1g<4>W`F3T{$#I}$8$sPtXR`4$~8U2`0k@JTj6+2mc)92ujHn7=6MA- zr}=Fkee}6SI+yuhd{hErJd(KX_nP3)1DtoGBOFQSi^0<1+r+*qH61+Jy(Yc%CUCs! zMCaFE(RI>MgS^)G-$spIvCbl`ud`GdUxj6S2y2$s|5(K;NVZYBAN@hzj8^D;^l{E9 zX?^vMOr7Z^oDGCicrpm6G?$iGxDE2fK^XA`d@(6Jo!b}3y_o(#0s-yW_na!}D?Yys4H5gCb=IrP z`|3lSHNL9ot6z!KIJt^1#$$tj;sLNL?gB4gz4c2+Na2f8(+k3FzIdC_49!#_=#v5Qp09#BUtF`s_iCfnmOC7V&Pluk=eT>-y@$5p@KszOPD7T+`4O z_beu(Rkji1b0ecD^A)B-RxEVD#`Y$@`UpCmY(X<$nPiq}>8p0JUw9egi=NFySk&GZ zr=u9`*wI&|WaxmdzIs-r%HlC^NuUf9Ou*^FjD1GUqqvavez@4At%EHer%NRtv~XXFXl5Z$3hY z`l@=Y+lT?NzrMi3vcdb{%D5qfxb#+wHPLvSm3V}&K21c6PCiQ8)wo0SYp({O5dB^} zk4YM@)pL#WV|?}Ts5(hWRj+v;vLDy*Ohb#9zEx!7E z1&zZEKRfD*Puj!OV~o1T7dX8g@U!tSg`TUAHi@&WIaTbhB7^*DIW#*v>U@iB z)}oYt%I-Xy%uj!bL1#Q40eJ04M?PCdOy7pUx${;xU`0mBmj6mkQba)K{h}n+sboC@mb(Vi>w3zn|V=uLV~xc$vgt=MsL{7-yQ)%OPXucR&TsvKHalc}^?d4_Z6B!V(jh#qY9y`b9)7VO>Q( znYYJQ@x#Vj6WX`BpYo!Cclr3~Lx^kBL#`V#(HP5u-~I%NquE#jtdY6wZ}h{nS}1FsO$g z7V1rsq&o{`osa%~V)Fk<^6P|N9UnBn4=WJkdi&{*3u(owA^`7hYOFrQPbM|Dhx_R_ zzI15Yv8XF9`z@UAr)o3V+u45VFb%nDuAe@OO2?gT!C@%u)M7vPz=2Pyw_&n8DBmDz z9KXg-e}hyf`?1bX4IvKR=%-%rRgzBI{Zw9tPTdJZ5?k&;0Vu5Ze%PP6RX*gWe!B1< zKed>6^Em8CqpWV-=td+a3JZAemcx9vJ&P57K~DfmpYl^3*)HQQ`KjStd7hlpQGPQ_ zzs@xUy?q6@zUConHs_$be$pG&c;KhLQMam({cvi6@!A15E3UkTj`)q0w!Chh|0Ygs z#a%+zYQ0*hZ(LqH^;0)!$K%ia^sDFEAX#7e;SB?WCEmi(Y2DqQ{8S}Qv9rI9+dm)0 zIh(;cfML0ykzdpq_T5h{V?jP8^jBf*O6X| z`^Vi4NX=#-v`pd{Zb+SJ;eHdLz^(nT#CEWH#0`okkptT=~Q(EaB z>&a*eUq8&Py{7rAR?5VFbf(e!9P!#_An27p_jn^(nKd;8$dT zqj#|RJ12+GWXa8AJ={!G41ghu#rvz06xU>#zl;H)NV*g=xQA>r+sU#KqZFp=nQ(SS_Dx_bIr^U!Pf`jgoYW zKR(}RVjq|q+E1Ug(8*easWYOdbpG1uuLd%QL3>~XR_Yz6KMqPY_E<4#ilaU5H9n_v z6)w+Jf81s4Kkg5^eLm=~#__J@#$BJ@+0Y6x9g2vu!3ZXvQ^)h+=-%Pb%h4;4`+h6W zCL442#zMzAnyBop_ILCX>LGZu&c+Dh_T`)tSa{`kC(DZ|WC8yxu%ub<@& z2gf!D6Rkg@WpeJmx)}?-S#a}I9VWuF4Yvf*kTbIj37<%7d^(20Ikt;0{cMm)eT!9@G1cKUSWfzSFt(edVwE(|NiM%4q+D<}wEh zjCzg8`cE3CPLq>9+v&gof2B@Em zy1*jmo+m(DWp};+y-8Z@U#Vb#`iql*A0-0t z2{02{t9*do9jGG|g{!+3X9iUX(4S~RXtNH3V>XpVH7>*IEuMC=MPziw)(lWlO!o*Y zR>wIv-*789dpIJ0#*G5zBye-*Gk>=Rt?dE&s8gL+&ENoRNicXdBml3^7>o!HP)n(z zN236II@oBiIP`Un84y|)5ByDC?e7qvaymVmB3p{M)jU9dsaOl1*a}9bhyiT_Q~|b) z>ODsOZUYewcn4F^iX9bzDa=g>axF7-?i_$F%jCBuWKXILF!f+O+J~1Fx9%j0!C+aY zTYz+$YppZ?=#x2?V${J6kxQJ_74dR9PxK7XXLD#}s`U-P!45{~k$b&7Qe(L-XGTk* z2f$P+aSV=Hs4yr%W#P?kI8^$~LIjTukOw;JC!$&@?bZ|+ieA-vW&oa28OO+*to77O z@cxt50lYey!78&6dxxC2=LV>6>{f2f$1A5aL+Onxx+XyHb^zaJG{Z=%KLar1D{0}b zox{?C8mU+HhoRbHpusSVTR0PA^3njg&n|<2+kA|gcxi`~`tD|L5?4=BW_JJwrt?KlYBcF?zDV&;N|Y%Y=Z@lOFFY96(mQjU4p2pzN#NN4 zY!Nb%kDcEc_Ys-AO6a<~V&oPV0`vg^x>EaI3Q%=f4X3UJVEefV9dk1PL!}XM{ZFro zhhdg4*Z^m4&?utDodEq3m=;mwL4ZCeN8`0es8n|PGl#y+t6v#f%En`Pz7D`$_Q!NF z=$=Y%wV@Z^{{oKBba!5%)XeYf?8)c#`^_EjjH8sErPD5PZ{d859QYwXHRXf$$@nxz zH$1kG*bnG5EZFoh{L|C2Mrq7Piai8b2$#5MD zh0d~<%zba%wSCB=r1W`^nA16@xJ_>I`;@ZjbDFe-8f9&=Hdyixo7`l6u4q%9DNTgr ze(v0YZ8n@qW9q8WXRls*pH=}WhPForXO>`_tWabhShwm9Y!WG+)z#J+<4A~2UE+;N zd_$XR%yRr_ViPA{)!e2}jn=x&ZDqrJ)nEgRxLdfzHC1byp4;k8kSW{RFw--bw4+Vc zV`z&`##B0j>S|N>8R2mc8}{g!xL$p1sv1i-Xt+(kVy$y4HQFXOk#S>fI9JHrg%@t^ zNEKsk#7cWHD^F3~pXHipQ&ZVODzrE`p&hKyLZ+7{27Dt1V`tYU(RePfu3!u%c8Mq3 z^aoCLHUp;G)KQ{+1`N&?n06K_op^S(O~1UMFc<&OpYCy*;V&(3G^&4l_6*f!=O)djh+Vtl>kYO`> zn>r(-kmODRR}*|wEJmeszFKY5TfDVOeb(CWbxVVpFv)Q(Vqf24!;+8bS0APsJp4UG zywwx7!j|Vbo5DfU+ilqSZ?dTUG3hY9r8*NfcA){3bl%u$lPSx(y{K^J)4^#|9yC+$ z12%P;1_}S$h7|^rdzK?MwU2o9ADi4qUOi!xwV_mJAeRwtpT&HQ!ctw(5{xEWOZcv9 z4NV>-bxq-?l#fAYk@}KN&7hZeoSvh3ZPf2)O!C{?q3g}7HXPbzQa|(mgqMD>wvxzo z!>&`ObWXY9&T8K+o2(Y?x$Capf%k3ti`?3l$)BK3DQ)ca-^WfNr*3FZcsz*54Tan?CtVr;hkw!>u*vMXmb`&4dh}n=e9DbS_6-RHEQJUu^hZwn@e$+VxE|KB%GzOU3GA{*Eg1^pTE9%4)Giu7&wKfKdf5It zmH_{waTsDGb#_Q*mnRHyDebBN3;A%`;EDf#g{-OznLCXgg*4igp4+{;ju&hlz;5}R z*8XZSSQE{Z){djKX|0DT)94T3I@otru&yFTW@nZ3cG*(A)3ZkKt= zhLU!vBhNB+eO{mz`Ldi{rK1;DbDny=5ptT~D;vhQV*O&UK|0Jg2K_*IMZ4Nf-BMS! z>(5f^)Vr(NWxr3yIvw)s^PNB89BwoBxr6-DSaTpFgG-s4HSFpGZQFK2OgSCL-%!)ytq8l`{H}3sjN8Q$7Op#D4K)1d zo;Rc8@^CI}V^^10hPU5ZMx=uQ@J^6kAdNd}Q)!*+(8*Mwut%tJSLeFAt6d+$r4{^^ z<$H?axChlIaC5lat`a+|^t9vTMiaxc<%s-CA=e*AlcC>q#jsL`z;~Qa`r2i* zn>)~s-2^7d-$Um;(wj9~c^D6tB&QK0n{}#O4EDmz8Df|Hn#G6PRZ5z+>PWjjRZ*Ay z$r!r~o{h#M0;lF}unXO_`}nPY@m>0Gk!oTQCp1s)Y(CpA&zVll#dyKm`Lxik-*wf3 zlgHcjF^L+7E=H?lgvm&Jo*s8NNU2m|^xxRGT^=I0F&Az2y`l}eyjl?Y- z2Dj4A=7;Pmm?>5rw(CO)bc&-V?CJ*1vf_+gR@`2f?e(qzj-zUq!=>jNM9bieKW|sQ zsf_sI$=h~(ih4MSdut;;_g(ZP)S~!9yZ#ih*5X-;lUZv*i*weU^m-<$4VlDx^VF{I z-5a81JG(r#>vLeWF1rxCn%MS*T^{{5jm}y=3L;9#3U<6lAa3ghzqVuP&ZJBB&Mr$d z$3NI*QL5HwbbB=C@n3dX;YLRQu-7Uk zpR3E|F-Qw6G(Jd*xVmO#~#N6G~{0`>b^x-=E>T7~PDWmla*S+sszKTw{l z4{8{w50uk!(>4uMec7>1Xcnjza)Gr(OrUHd`QADZTlb7s@6+w??u&L`3U^4aDxnv7 z(>D;yiCsGeV)VVyK2Tv4*4kIv{ORWwG`|-3CLm78!DsAnEr)Y3;<@hndv2TivOFYZ z`(H`>BlIMHPPaf+gDPF=9w_@j0{aB2!3;f$Y-iKgQVbvc={BmUdJ?Nu)gbhZp3!h< zt3{Naf-W5ts1NJY#helwsCqDUl3{`R{8k+rF)C2aWa!t?f$AN%>%5vCs0J{!fyJ=2^F#3?GKc4qEQIq7fIPDk9OyK5)4GyHU*x=*uI zyby?Q-Wm&eUka2hnrm(Z$_|Wiw*qAg!k*%*_vC?Yhm@5ThRy42_1Il|@lOI(6XyT( zRiKKeR`G8F)nOjM(C$N^UOv{ZfNlL8h!$uREY_i3Mm=%&fpg$&O}bBN=j3mJY63;% z`Vpv-@Xn;LM-c32(ltofbJlZo3_X+mPr*(n3zF{Pda5A(+Pkj$=4pb|b?R0vQ;_Py z(6yO^)Mh$+=QA%B>QjP_DSDQ3a`qtonzK%PGDnc?<#FEah6Cm?JGQ*h>hf77^CCCv zR(Cy+pUNGi$}ojOa4tGW)ovYHg(5}j1hkm-x(A%qnvdTU&WOT6sv(o?E)t}!vQB50 z3R0aJ>QN?0m0~qj`6EcVM%rZ+g5;T3eKZ5t4yJ9@gH-<%rZ-%FY=D@3$?Wg5$ zjViOeIF8gml*63ulit$TN;<XK%WUBa3GIZ4RSh zx*T!+^lW}mry%vXfiZU*Y&URiT-kF3HOQW;=J6o)4{wo^T?*14=+RZv_$q3U!)luQ zLHa9c2yNE>b;pMLp}Z%K%D{tiW|Z9;JV?*Nhdd0DHS&nZK}uyaNy4ji+HeI)#^YuI zho8BUIO9~XK2ls~H7{YXTAtiQ&R+0N|AM%S&yneWA`eU*tZa<2E7;?xe?FY?9!wKW z-~T$3Rbq%<(aV%IScT~*R!G+6z^k;7wLuqsWgIt_8zO_z+WcPlTN zCZ-Y*)3d>tQ{sa0eUhdlbu-B}J6L}uM4L2lUN9y%#-uk({Ayee1xk;z0`Z|>d@&D6 z=JB}hl1qYdy0AH5ZN;C#Y6R=M^u}N{pFVbSbFfOxV$bjn>M;@%pJJZO+8F-L{$RAy z4rBB*pxDk}HJRP-9t5wWuujK<)nC-4=CCW(i{W|X+YFR|t`fE6VY1(MIT*`Hsd0vQ zvsQrzZal|(jdyaIBe1YD@LI6SPGg>W9IU*U!t+hAs+!Cs4*eRe`p}n-d<#a$pGwQA zdn)yb&NwG)$n*pIGfWTvkksn`0~d{bJbnhd_WAAY;=scO6Zk?o^x>-7em)5u7$i-L zKhUwv?WXYGht}Ny2Rf!!Q*=IMg{dPnCXi zg;EgVhlA4m9T;~gqxxiBYrB&>6pn_`t!sOmzK_EZFWxG9)18&GSW1UJ#yB}4p_n1pbaG0i5s&zWG3-8n?s+ztc^4`r$gofX>vQ{3Fq3Ub%VoDh__e< z!plDCEziQb$P;Jc2Gw~buR}FrVZS}<_)(vM@yc2>S{HT$E~U4AAzBLOp!^P1fVN9D z=VWhvhc_&dbr#2Ziny3rdY$Wi0SE5zjGLEAJ=IIUF&WLJBVdTZ`Ab=^&V>&-6IO7@ zNY%We!!ffujS>Xqkh_&(h>ZB%VDyhl7=+hTWh2fjdH-Wt4m zqI_;^{}2b(!^$kw<5MXwhsr~(YI{3WXO^ky-RRKl(5f1~*`!abb>8vO4Zw(a^Z9ZX zYdoB_yt9*^LuF#(Q3!y~Xxm&yV;Dd=<-_|lSOz&bU#Htzt=5zv2k!nXyF>lOx~`MQ z+iMhJWHWmjGgvyKG}it`4%|uP33Z_JSdSzwyESz<)MRGzGQ^=qGLr%)ULJ1<5$7@5 zZ$&0?EghJ`uMN}f)F@Oc+@V}kzCE=aGD&J#*P+%hLhAYswTigAfdjWUreeDm8IZFa zOSO>9Kr?7BR}odlj|251KV& zq?WB5`fwp_FV9G~k1U4|Y-DJY7+o7C!tvG)6-x|j>(1s@JJde2DZ1)P>LJKx51$~R zM@#LT(m{(d1!;aQI`B4{ep|&kKwNhRR^~=`a;OhR{@~6Iyar(0aar+9f476ATUH)? zd^>}VS=0($smJh+U3Hr_0$ar%sx}_6N}^i3V*0ob(QwS4F3g!84jjW`7OeB9yLZLQ z3h#ps!ug_?+fu;@Sis0n`#NM}g|k2GLmy3^{M&l#t%8p_JqI~(Or#N9Ww1jgmb>A< z4XAI^KRT7y?@S-&RzfR&oI|Cvx~$a?L!yQvXHHJGT^iE;8!T`fUs*8o-GAdHab6wn zP#KxY+Yt^G&NB81d)444lAJ{nbhCM#T}SDj$LKN}-BMlR%Q4z^<~X>J43DEK!79yN zeR*7-S{MOuuGjs*G3e#PE;aQ8hg!?T`zAVMpZK214z-lW*9@+l_u;0sO5vb#hSaXRLhJoHI| zXAb#1(Qif+<%@6V6OoB)J9*ebhb+XUv>a+HW!|cm0o&a0Ax>A9V4UntyhGk|YPr~< z;*2KymO5~{pHbw}nUibkAQ3(lwJ38|C=!0qfyFxSZ82E0@L#T5gK@j+|BwBHKGNr! z<%t$t#M*}l`JG?>bf|+aORaRbk$S7q3fTd+MxWw(U4MC8S3hn1Nw}kFtEWFEr)i%& zEN@6mWLZ1(vQx_qx*CjXhk8bQ(a$6bNw@uf5@gw_N2O02-6L$<%?>r6+9ZLKIMbqm-qp^vH>fY_8eUlmmJ^ zYIez?D$*eLKPBE*8WHRX*bcPnS)9i&J5&&}$c#SK)#v_s%^@2D3*U68EsSvMmP1V< zcDdtF*N7qa^z_SUmiK|XLympu?$&QSa!>pkKXG@xH=a7wA|{J{ZiWv1T(9&?huX~c zKRD;BYxSUSoU4Ucp6_(S&GXvbkxzZ&z;c3dziZxUr*=o=N&Gkp8XzLOh9l>Dhw7@# zxty6VE^3wL5y>8~l9t968B6CJ~(4AV?NRcQ*&;;FHkcR@f*UrY?wK|rvV**L$%i%n#x2Uobml-}jO9kD&+S9hLrS~dJw#5p zIMXXcbxmoCnSP9?|05LB%ko*K50Nv;to}Rv_AdSUuMSb2X`1I7L(~oy zeEH50m6b2M%YBK4WEeM$>gpYwDur|r}Od;)IV<}JzJb8-Vfe4-`u6UD$K;Pf^TS> zFM?kWr?Q2UhHB&Kc02{M@IzehYM3Qd4We}~XAf1oX`A4?OFQWu?3eH<1oJ*RU17a% zDDLx47YNlKXV(=HUN}^rC#^AMF`Na)wS#Yd3u5&tA5T*K*Z(Y&+Uj~+uf}vJ6Dlv+ zJ+2t4KQo|ZOsyWOo)QNktLwz3fuUIIG7$!bg{m^F$@TR^^}+KxS=vUSvQg`4lTbB_ zTSszbtN;EYyz)GcKXS%LK|#97_BNqv5hXV59ID!}DvtCHRreXXc2KDL$c2`nL48tw zMpfLvDxh`~=jAb>Dl4mL>e=ou(!)$e{8!3QD9*qL)#e z`nrwQJ~};AcBGY_6{=6?)kXQaFjW1K*mO#NED2RHRA<3T6p)R-=ju@PfW>aQE>!ka zXWtkqJLVhh2$dK5cJ2vP|0FVt2^Yq`cyI;Z7dcnhd^aNf^gLe^=OKA7vYMZuS0_%Qgt z1^R&h!S4TC|64y9UWcin4DjBl({4t<5EGYG9R4N@Cy^L+TsFr?aPBA4i&&NkaQeSP zfZ1+jsx-S5@F5I43;&bELcm}K?7yZnsrE4phe&D#H-6Sdx{v>(sfFh`9bpXWzmSgL z)SiI3rjENPiT1fXU&8PbfsyVsRon{!{tSr!teyNa0{j?YOzN!sH4L{l|B>+x2ewWz z|2GuSGFN;L!?Ut~^=bGc44-cK_y21A=k;#T6leDQ40FxIT}q|L*-n#mXW?oXPW-R% z@BfAAYPtTJ{1(R4QdJWnw##kz?yco|7saAJixSjQbN&;-YKVhHW*K=jLW)GS@b=KZ z`EAAj|Kw+RB$oedNlo#;w27}<9SV&9)tSNGFWP1aVB0`OFg4N^|0}vPti!0ZciN=2 zU~i*6?cD zUT%%DTaM(l;0OO2(TYbvH4}kG{EGiBD^t(fDcn{)(?NS#(v-FEg-dNkQ>|7M{#P;o z>6W|jpGM&S)T!{FSx)uS0wNJm!=-~M`CbH6rUYZ>^r>Oz|K!jK|Cs}GJoJD5XHSFc z6v)Qk9Ra>Z04x3+{#P>pS;)e8S)%HHG8lsY%z(}J68=~CugXQ!LS>VHWg3D1EF%+K z!GBi*cB#eTyVWRxW*mV4S9Q}N9wNZsh-Ck1PhSiD=YJezJp%mxqqJv+|5Gz<5#SQZ zPVguKT<$B>ua~May1604>W1l{5bv;V>%K zIUWJ$&8TigWUGZ;b^5ME4~;*PvT$$As5KyeEj&)tW2#lQa4lu$aC=}ZL`7VBMr-lA$sef82=d8jJs(J@A z5?!1Yfze5il0~LR;1L^3OMP{1q+ z+XSj>Bzw$`z$cbMT%*N%0WGd@Ie5!1ED(H4em5egV}pVYL>B1{SEU3Hu0YVx-hrC{mV! zc=a2Ic8QD=TO9^9H)#u55jdMJ7f>VeKAZqZz9aej-l*0+4l!EIbT=TBEpq?hJ=lTe|0-&Xg1hKsD zK(ubt4U*eo5KB~Hqexx?VhYbqVxi$6>h}u7_R(Z>gz9ASIOe9RTaeZR`8K<8p$jP}Pt%(u!x37ZFE9tjSKQ?>>%#Ssu4 zu+ZP4-vkhw@(WOeksN+lu5JS{EZdO?Y{A#=)Eg9O=sJkbTk{`@w;05*#7D*P+khB$ z3`AXuACnZ5LA2FJcUa@&Qnn)?dP&(65@vzeq|`}~7YU-Khuu`{lt`Z9rcWTIZFE{n zz60d)k~1P@7^r13?Z#e>{_Z&2Dhu5*TE7v>&MWY&SYfT3(w!4IeL>Xv9*EYmpBG(M zfLISnFGziL0a1sGAf~8#QEGh=h^0$>N#b>I(^(L$TJEyMn+0lN^uJf{P}wWifh$`7 zboizIO+l>p-5|=&eN}QE0b-3kc2n>*NwEsV{8C((c%4DC>lqNeyV?z@v6Ud^mh7fb zI}r0d31ZsPx1@A4LGX-MqNEr>H3Y1wKnc)0ME+%Is&s?-9NMiu^&DSqMSe8h&J;;Y#E8(O6fX**sos$ zvDSUxNl&{DM1RWqUL?nY81@{*w2eN9yaR43_EAzyb<-CRhp*^Q7&}Z!s`zK_7ig7P zUKb^Ki_kz*lIowuHt`_ZL4A?17!YlJ1myC@uX1%NsFj%neE>0DgKwhs0T64p*!Ku* zEYfyc0%~Vcr1>HJetQrH=c^zN=V3o(NIn3frHlL$OHTyRquzj+N8{fTZ$HRP>lS8; zWOu%>dnCu0g(o9jbEd*7($!c;gQ)sbcUVw@NLQt<196^`B4MN})6Q->3!=%&B@&tr zV#oImL^bOt7TN*g)yzpET?HEq;;?fKL}&6!8tJOQ)gU@kMh`8;%SUVNER*2{&Dm zHc|~R*Yl=}#A=6*xdFt5qZ;XTn0BiZpecr0X3$r4*bC4ClVW_vNEL6W6g*;)p>rV1 zP|wVfIBP>o&YC4sjWM(o#N5hfjZ_m&*g?>EL$$L-Vx>ptaSs%0C?9)X4# z>X0K6XF2P5Z$T{an4FPnj=7pYS0oO&)hV2y*@hbDj>M;%bc#=)5hiR<9?{ApZ=@P) z!e)R*8_JbW%DWmg#H1~mUu&SHYyu57RK9>%|1gNUG%qOD{|K6HRE>0XlS@F%BW*RIAt2Vm7ZB^H zXLZr<8Hl>Hs3G#sf?OKZl-%}#D5a8DB+g*f<=6(|T6Pg{(R4Y8X><5!>*{zDKunRs zSNnmktuY|lRFi=<1i@klk zWNIp_R`Mq3#XQ3zF(7FhwFXhcYal*ws8dUFIRc{fy~8Em77$BPptj_^2t>POuOlUy z2x5&StSdgz4aDB_HK?D_phG>W>w6%2QFwjHV?T&mRc;_!t#wnrhS~#lIi`Zxx)L=K zuj~P0+UFqFf7`}Vn>Rr$dF>|BuN(zYXRoGG51T-=LGfm~hP4;10=^1%iBhJ^0gp_`L>m?Qy_*#wUe;dpvflf(Du@! zChwrfAuVq{Xr!V19VP7!5JwCDPSWFF2XPE**V(Nf$faKw$s<=+$>UED^R3cNMvJo` z_UTQ!i=v_jA1iCv{1=ja&@npLVF{g2g=cG#OcoOt(Fj#$$E`IHpBLP zMD%A6%{#iUD3iONRLD9IClj{*5_Zu|(F62=rH%Ck#9@B;K#7-bkeG4_h^hb}7A@dc8CXXE{hE6t0 zq$~h&=r1%{wAv10^?Qwx>c0SDZXL%;Ilh4CSQE!dSl01UHJd;zx$gvVn5!T1A#S|Ylv0#U!3OT|{FK%C>nERza+4;p54>APIYmgrB(Z8V5^q*x&(ngtqS zMl;VNd3}7Wmh1GKy!=XiccIHPA4FFyxJqWf%R#KKimRo>dqFH`@EXbG6o{S>zEiTHcEdpQXwS`J zkeML5N8T-x@lp`me1)yzT6;k(T*i7M(Z^~+Z)p~+b(GyfaqpD zc8HsO1yR-EJ4Mw*yCiH9h~~<^TWAG{)l*`R$lDEKbMe_LB{>XYN$TvAl3WBaZDXfY z?E?_a-+I5Kcn6}PDpYoTIZ;!avelJX?jf3-UBgh*W;4*qnrAl5dD&z6#d46=q1Td zNo7p~u|G+BTH?(BapvN2M$9)IMDt}lDz^}GjBmtI#S4|P@Y7y+U)rM)Kl%>=Phb6uBw z<3Q9t-whdf7lIg;_om#5EdWu2Lbt?ze}d@Z6>m$O?*Xy-gx-;ITm-TC#N3s7djVqD zz(N2_W9nrh2H$t8FkB#Fc;|k0jnE5LW`c9*d^?LCh`uiS(q8 zK8Z+jtk!Ph|4pxsL?PiyrH#Cz%4uf#4nUyHmA zAa(*(--yRLK^$S4y%qWj;)=nHcapa7ds*8!3}T9?4=#%PC<*3+=tB8F$(&*-O1>5}%5*jri$%E>SXO{|L!7I4lxu@SPY@SrFM_zBU9LowYvFqah+%Kt z6jn0IwRpV|#AgXPOGUYsb^C$%1o^f@c{mMNbxcfVkLv2IN|@ zE*s@~E;AhzZQ^}%hqWjtIy*s3k*mCD&>zI5=qn&D;8*=abY2K*Y2^I|MHz~zpmo-| zTm(^P--=pi9kv+6r!A=}iB{d*bP>cnDpeM(=79KA=?93$_CeJLv5Q#`;#z+GW?C05Zxo2~ zo`AT$#IL!eSO}_bbomJ4bE?`cqzhcjF>`xFo@XT$sJZ-fhpt>gBMNp&(tJ+S=HXX$7=`Dz@yHR`T40pOI zTL+Oh48*eC0l9jgj@nCfZmU47qm-SrznFRxit8MO8zLPP3*u7qdk~kEyLOQd%0lX(KdZ=NzvO)S3#~Wv5z>|ED+uK4Tx!L_mw<0fat_2`bpYOAZl;|)X-F0 zq5k5Mqd>I5QxN3^50I;?K&%*#ffCjhM6Hg2_<+lAkkB3wADxvN9OZf#xCX>U_98>X zO{aqR_$EoL&;Sta@)X2JiV;Jl^EnKniEk%>SQ*s6mTGQjUK>e6;MfSeHZRwirZ8Q_w=vP6j#2V|}lyS9KasY@u%?%LC=Cwv_wI9T#HLtaDZ}S(37OJpLG}sL4XzY@2 zz1U?kh&ulUv1U4LkUObQAl`8e-YB{K1`Rb*hHsKy;46sb=(btv{Gpo~Z;`N*Ao`Hk zR%sPGLF~f{Zxc(-2bl?D+;*AyM1we2Iu2sfs=PzS!*~$KzC=64)nY)LbshsT+v>Zd zd!7g4*qmUuc4F-=(I7UXqo8`Gv&ge2%Jp_y4-gCf8AONZvR4;Fr+wa+{ol<|OsO#;y+ll>+6P6Wl86rKkqYzBy7nGZ=bnhRnx%J#P& z>~$XVKtl}WI4oBef`*x}d`IN=cqNEkd+vXvI;Ml@8tSN&cL0bE_6o$gQq(c&!cK$O z7;VRe_JWwA%n6aQ3dB+tJSioQ12HV?DWMr4&b>TNi|u1Ul=mLQ+}fWJJ6{7atoB(w zxzRQ_1!8?vJ11$^fmk1T&I?Tgv7x97BCi{WVRt~R{YDps&VbmAye|oD08w(j%R)0i zu5w(Fni&J4roTaK$b+wn&fh^?Fd2DGD9v@T%Qz5i@Ds#3>TpByxD8@h=uHVb0HO^_ z-4dD);?R@yw$$Z75Ys*bu^i3rNN0N(#QUkTcf~I8AeJNZJ&`iOP3pebr5lL#`vOGI ziFhD%2}J$;9!l5_5Ph=LBMDmqqK_ANES|Fv#LhPR6VYWFh^;KeQz=nD5c7QnVtqtD zld#JmmOS9OSa%1A^;_hHly^3W@e;ojox6i5`7wz8(%_Zkam-CMUrP(v0b-q3dL!Cz z1+iBt{Z`7c9K?79-|6X#_UA<)mN)l%DdijxOO*11XfOao?Vp2Km(4#4od&VqY@bB? z-5^d63Vs$TGeAt6Ld{5eg&~7>77vGy#>(^yC;fv)y5kT%M+0}8dF?dS9?ITa``0DE{|LTV*M3M z8ttm_r6Be$IXr}Bfaw3plS$Yx5aoRZ@oJakn)IajA*hAP*OnsMRYRLVYzf6vM!T%M z5JV{%Qb~y>gIL~gAeN&;>S&i&-3HM&8>bO@=RnjT&{Hh*H;Cn^kyheu12JBSbkVMQ zSpcHHdZrhxhJoleA3>DTDT8Qr#Z7)0qg{Q)22hkKZ>CIAqKTlk=Do--pmv6$GwTxR zcTz8cdK(JMqH9`*?FDr*R5Gj9K!?SH`WnifO=uRVzX?m9U2Cu7jRy5F^b^FiU2_QC z2lX*mo9C1~PJ$R_&n3F-2QjQ_?r3z7}O2Kwu*y zFbDqAMAJaACM;vgXiRvtsN^l)DJ`rzJcgYz5bB2&p@oMW);LePl6ccRZ)zv-A$z{iGGVgjF+LZ z$QuiyLw|9Hb*duyJ^|5IEvkyG?t%sw3$?5kjb~%pYA-;840Wt7lJA0;wqXqkI{~8G zdDqlEt4_NL#AcS=OR9P_h%WIC#CXlTMf+1Ax@R>XDeqNi5poekquFf|wgp7N`R$_o3=rKQX`mFN2Z+kw1Vx!V{DS1_ zdJt3O3l=HULDWDw#8kaN47(4aS;9g@$}SKk=Lr=}r-9hYzJqA0wqbJh8i*~lb}b1z z2x5uKg^MmrKx|RjYYR;WF>UfXQoXSt_JnUhv{htX2|EX(l;C?-PER`SnV2!j^k(~wsL~l$o^<7O?9oC3N;b?O?A@`5M8osQ>m?MAoe-7 zW@7s-AnI4Vxx||ZqIDCr5M6qJ*y~+)lV3}bw;sg0%-KpxJ^@6XRfL3f1<`Y^ftZIq zQm$?T(Q}GM2`vDz9G=mVVi1V-d+82q5+h;9K(t@A)>3b4L6n@QjnH&ZG}{M=Ued0u z?AZ4#7^Zihzoe_ zJByTyAXi)MBDB*@#k)!#@gUY^=59Jn&w?g^YMWWX9Z)@^X{GKGZyktTanT+^OF_d- zyrMm$@pxKyW;;Ox4OQ+X{UqXLi<20`5z;s-&g~pE?Gv3|BVLGrXN5o zTbohh*_T1|rjXI%2m3(urqW|1o&{pO3}c1Hg6KovK^zzQjFYgZAkOAmkC$>h0&(bR zH9_bah=XLKiP6|#pzU%S#IWX*qOlP{huwEmyU7yv8bmusO%b{ba+PSRXuk)5>%yYWLiv_XOz6Q~1&1Q(?Ga#1Nccxrj4`Pa3v&6bnLF~tq&6aYE1~KfnoBGTV zyL<-GJNwR+R`D6cI`216u6_f#raAMYv5i98N};gAl9mH zoYdAnkjsZG(R34t^;SAwG}s6lZE~x&NOai)Vu}7(EV(TQ(Wcp#$RsEZG{&SYx>WLA z24cS1mx=AigIL~F%cUpm38Ejo1JT1e|0!+yF^DB^xm=S?5bLPfdTIOT-4wh*eEckkbzWzq zw1pFHvTYK12SK!RwawynD?s$R%v&VwRFDaa+bT&1f#?RWLF}%YY!ibY12L?^b`iW7 z#4gcuhnQ+Oh=qI&;>}&#onnHUAi7o9E+Hp~l~8WCm|!J{18mkkV%*svR%-gaQkA1Y zu7={KKKn$ghagVZ!kj|eK`dp_{St3Bhz3e>KpNCY5DlK>FR7xTAeQ$vh!x%Opp^0< zh>{x~66d@M;!xJ&Zz;zO5WBSohsBZyL9}kEBhp=M0nt_k|B zydd3jClJei-%X7#N)x>bVmU)DiT?k9D7nUEsl#0$hLye|_E-d>I4vL_!o$} z^u8vw^cdvoV6IEPr$9{MeM5ZWFA#@p+fC8*Fo?eGdrSOjH;CO?mD@twL2OfH?ub{e z2GLJ)+?9MMgV=t4gV+Yz-jh4^gCI(&eqa1#8;B(;^+4)v0f-aIBoARL*1GNZmtt1g zpq8lhIE2q(0kN*XgJ|^^|uf!fZK&-p{h{IIs_i}YQh*Q~gAB3iYm^RbLXzYR2Eom``X$yQ3t>%DO z*U3Ih{f_{#=6-;vY4f5H+cnAjahn zhd|V%NJ634jwW(bHkC7(dFROcj;$2|~hBOb}mE}(*G!aCv{{~_n zQK{wXNjFtWBUj@=%r}jvje45^xZ&I?-ht0fBOPLk3cj_^MX>KlOR?? zwL+rkIuKnlS7E8Z@gTbH50Gp4Eh3e555%H1EGp(Z38LdvFD3@x4dMXtM{$w23`Cbq zS3(Rq3dD~19f(pQN=k}zAm(N-C26;S*r`=4EgGx=(U}UA5h>F_bhX4~#UX}(SfZC8 zTCH65bUZTM|5U&>XmJ|y>u5Qvt>^A|#d=)+wOIvkx(`67faQI0nce|;$zsOqz zq7BjpNNo)PQLE1&=G)08x!nU%as#`hJq%)9RtOZGSAeKr&LBxK6~wS4!IE1~5bg39 zM31fKkbIAU*gmR-NZJh`rpO;EDW-s^!Ebk1$1n-I?xx^cBE<=!)ry6Sl-VF!BWZ2X zuNR25{{qC;)2NPg;|D+tD_K{h%mcMHlP~j0QfouLR+xO{suzRf`1G*a6~vDG8i@G? z*B5QJf+#U>1Cclh#KAdr!x-1fz-$m7+GJ}agR%vpWzsbk$-_Xj%v%s`8QDZMI00f< zjizGr%^+GPS2MXf8N~Aqk~EKT?eb|3s$)vD3BPDLBGlB+vGv`f(li8l(wuutx=x{*?jy`Y9hUgjvttrw_;3A+SpY^Yqc zXgUW(P2Yf6A5CIJ$`(+JNs*&<40coN67>Qx?4~=cVjD>t526O&Kx`qM+s0s1tWJ9p z#IzOKiM&Z5hJA8})oL#`*a)J9Qg;wbb_3Cpr$N-YO2-)2dU70yrThk}XY#GxN&1W9 zAZ|M>**ON5)VFsFLDcy(D8i&@+C|cC0`cZALs!wI8;J2Pf|y5%ZW3=2sIHOn07OlF zyGwcJgXkryhuFR`h&pctQC`ZP()Rj;c=Zy9<@N3*I?n?!k1rtBQSIK6)(PT<{MG$*2-+|JP z?gDZCP-&3lwhZJNp9hPSK_G@b199wZG(@f*1X1V8v0{zIZb~^+*Q)llfgnnG3S!%D zG)%^!V<38G&Ee9sZ3S`MEj&W5&IhrSDM!lH!61$dpFlm$X5mhww0G)l!)HKjZ)Hb| z&I>@idCoINq)Y=*gCt``zh0m&M#^Il(*_Xh+;gIMQ%4Xz`;?o?OpAqKr#P* zAg|Rze1h0Yi(k8`xTc^;L;FFjkbH5{^cH})nw8U%DbXAdT_4u9$ z>AyfUN7==aVhM<5OTI*^usw*yya-}fk}1Tf|t)Kru%9R9hwOaFFR+s2|6jo-3H>UHR~y5WA^wZfbo=EPfWm6g4kP*h&yx(esLwya$N7Tz8Y-Rk7(}5a*@8L3Es` zYceJt0?~ejuZzo01yTEN?y%?^+E&_0j)It5*_%RhK^$YhftWVxmV_MwG2bG$#o`k| zwAC*+b-5#@yym8WyOLrRh_#UQo`eksu^V{hrWW@l-boPisQEy`)`F;g#)ncf13|2X zCm=T4I*%le?I1SWtdDhC?Zrbuycf9&vYSSm|49tKTcGE2J=}B<#5r=dr!n|~fxg-n iRL@9pf_S%)>zNE|gF#`&&Zj|UF6}J-JO Date: Wed, 6 May 2026 06:27:49 +0200 Subject: [PATCH 9/9] Complete secure config example --- Latest_Compiled_Version/config.ini.example | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example index 4292b785..cd8bd750 100644 --- a/Latest_Compiled_Version/config.ini.example +++ b/Latest_Compiled_Version/config.ini.example @@ -10,6 +10,11 @@ db.pool.maxsize=100 # Encrypt your traffic crypto.ws.enabled=0 +# Optional packet signing for encrypted WebSocket traffic. +crypto.ws.signing.enabled=false +# Optional persistent signing keys. Leave empty to auto-generate/persist them in emulator_settings. +crypto.ws.signing.public_key= +crypto.ws.signing.private_key= #Game Configuration. #Host IP. Most likely just 0.0.0.0 Use 127.0.0.1 if you want to play on LAN. @@ -48,6 +53,8 @@ enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd # Nitro secure runtime assets. JSON files are read live from disk. nitro.secure.assets.enabled=true nitro.secure.api.enabled=true +# Secure runtime ECDH session TTL in seconds. +nitro.secure.session_ttl_sec=900 # Point this to your deployed Nitro `/configuration` folder when secure config assets are enabled. nitro.secure.config.root= nitro.secure.gamedata.root= @@ -59,3 +66,6 @@ login.remember.enabled=true login.remember.duration.days=30 # Optional: set a persistent remember-me JWT secret here, otherwise one is generated and stored in emulator_settings. login.remember.jwt.secret= + +# Login news API. +login.news.limit=5