From 32d2915b7811c782cf2644317e2bb388a07cb99c Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 26 Mar 2026 05:24:53 +0100 Subject: [PATCH] Add wired evaluation and text placeholder extras --- .../habbo/habbohotel/items/ItemManager.java | 2 + .../wired/effects/WiredEffectAlert.java | 6 +- .../wired/effects/WiredEffectBotTalk.java | 5 +- .../effects/WiredEffectBotTalkToHabbo.java | 4 +- .../wired/effects/WiredEffectKickHabbo.java | 9 +- .../wired/effects/WiredEffectMuteHabbo.java | 7 +- .../wired/effects/WiredEffectWhisper.java | 4 +- .../wired/extra/WiredExtraOrEval.java | 255 +++++++++++++++++- .../extra/WiredExtraTextOutputUsername.java | 212 +++++++++++++++ .../habbohotel/rooms/RoomChatManager.java | 35 ++- .../habbo/habbohotel/wired/WiredHandler.java | 61 ++++- .../habbohotel/wired/api/WiredStack.java | 34 ++- .../wired/core/RoomWiredStackIndex.java | 18 +- .../habbohotel/wired/core/WiredEngine.java | 185 ++++++++----- .../habbohotel/wired/core/WiredManager.java | 9 + .../wired/core/WiredTextPlaceholderUtil.java | 113 ++++++++ .../outgoing/users/UserClothesComposer.java | 28 ++ 17 files changed, 883 insertions(+), 104 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputUsername.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java index 5ee05fe3..d724ce14 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java @@ -60,6 +60,7 @@ import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMovePhys import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveNoAnimation; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputUsername; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; import com.eu.habbo.habbohotel.items.interactions.wired.selector.*; import com.eu.habbo.habbohotel.items.interactions.wired.triggers.*; @@ -353,6 +354,7 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_xtra_mov_physics", WiredExtraMovePhysics.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_exec_in_order", WiredExtraExecuteInOrder.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_execution_limit", WiredExtraExecutionLimit.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_text_output_username", WiredExtraTextOutputUsername.class)); this.interactionsList.add(new ItemInteraction("wf_highscore", InteractionWiredHighscore.class)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java index ee4520c2..e149b872 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import java.sql.ResultSet; import java.sql.SQLException; @@ -26,10 +27,11 @@ public class WiredEffectAlert extends WiredEffectWhisper { Habbo habbo = room.getHabbo(unit); if (habbo == null) continue; - habbo.alert(this.message + String message = this.message .replace("%online%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "") .replace("%username%", habbo.getHabboInfo().getUsername()) - .replace("%roomsloaded%", Emulator.getGameEnvironment().getRoomManager().loadedRoomsCount() + "")); + .replace("%roomsloaded%", Emulator.getGameEnvironment().getRoomManager().loadedRoomsCount() + ""); + habbo.alert(WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, message)); } } } 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 dfb0aa88..d027210b 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 @@ -13,6 +13,7 @@ import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; @@ -111,6 +112,8 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { .replace(Emulator.getTexts().getValue("wired.variable.user_count", "%user_count%"), room.getUserCount() + ""); } + message = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, message); + List bots = WiredBotSourceUtil.resolveBots(ctx, room, this.botSource, this.botName); for (Bot bot : bots) { @@ -177,7 +180,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return WiredBotSourceUtil.requiresTriggeringUser(this.botSource); + return WiredBotSourceUtil.requiresTriggeringUser(this.botSource) || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } public int getMode() { 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 3532b276..2673993c 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 @@ -15,6 +15,7 @@ import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import gnu.trove.procedure.TObjectProcedure; @@ -136,6 +137,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { .replace(Emulator.getTexts().getValue("wired.variable.item_count", "%item_count%"), room.itemCount() + "") .replace(Emulator.getTexts().getValue("wired.variable.roomname", "%roomname%"), room.getName()) .replace(Emulator.getTexts().getValue("wired.variable.user_count", "%user_count%"), room.getUserCount() + ""); + m = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, m); for (Bot bot : bots) { String botMessage = m.replace(Emulator.getTexts().getValue("wired.variable.name", "%name%"), bot.getName()); @@ -203,7 +205,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredBotSourceUtil.requiresTriggeringUser(this.botSource); + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredBotSourceUtil.requiresTriggeringUser(this.botSource) || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java index 5df7a21f..1803652f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java @@ -16,6 +16,7 @@ import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer; @@ -70,8 +71,10 @@ public class WiredEffectKickHabbo extends InteractionWiredEffect { room.giveEffect(habbo, 4, 2); - if (!this.message.isEmpty()) - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(this.message, habbo, habbo, RoomChatMessageBubbles.ALERT))); + if (!this.message.isEmpty()) { + String message = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, this.message); + habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(message, habbo, habbo, RoomChatMessageBubbles.ALERT))); + } Emulator.getThreading().run(new RoomUnitKick(habbo, room, true), 2000); } @@ -183,7 +186,7 @@ public class WiredEffectKickHabbo extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java index 443cb24c..e7a66737 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java @@ -14,6 +14,7 @@ import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer; @@ -78,7 +79,9 @@ public class WiredEffectMuteHabbo extends InteractionWiredEffect { room.muteHabbo(habbo, Math.max(1, this.length)); - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(this.message.replace("%user%", habbo.getHabboInfo().getUsername()).replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "").replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""), habbo, habbo, RoomChatMessageBubbles.WIRED))); + String message = this.message.replace("%user%", habbo.getHabboInfo().getUsername()).replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "").replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""); + message = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, message); + habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(message, habbo, habbo, RoomChatMessageBubbles.WIRED))); } } @@ -137,7 +140,7 @@ public class WiredEffectMuteHabbo extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { 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 9ac89433..4f8cb4ff 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 @@ -13,6 +13,7 @@ import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer; @@ -105,6 +106,7 @@ public class WiredEffectWhisper extends InteractionWiredEffect { if (habbo == null) continue; String msg = this.message.replace("%user%", habbo.getHabboInfo().getUsername()).replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "").replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""); + msg = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, msg); habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.WIRED))); if (habbo.getRoomUnit().isIdle()) { @@ -162,7 +164,7 @@ public class WiredEffectWhisper extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return (this.userSource == WiredSourceUtil.SOURCE_TRIGGER) || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java index 0f1feb67..548466b3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java @@ -1,21 +1,48 @@ package com.eu.habbo.habbohotel.items.interactions.wired.extra; +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.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; 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 WiredExtraOrEval extends InteractionWiredExtra { + public static final int CODE = 66; + public static final int MODE_ALL = 0; + public static final int MODE_AT_LEAST_ONE = 1; + public static final int MODE_NOT_ALL = 2; + public static final int MODE_NONE = 3; + public static final int MODE_LESS_THAN = 4; + public static final int MODE_EXACTLY = 5; + public static final int MODE_MORE_THAN = 6; + public static final int MIN_COMPARE_VALUE = 0; + public static final int MAX_COMPARE_VALUE = 100; + + private final THashSet items; + private int evaluationMode = MODE_ALL; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int compareValue = 1; + public WiredExtraOrEval(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); + this.items = new THashSet<>(); } public WiredExtraOrEval(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); } @Override @@ -23,28 +50,252 @@ public class WiredExtraOrEval extends InteractionWiredExtra { return false; } + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.evaluationMode = normalizeEvaluationMode((params.length > 0) ? params[0] : MODE_ALL); + this.furniSource = normalizeFurniSource((params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER); + this.compareValue = normalizeCompareValue((params.length > 2) ? params[2] : this.compareValue); + this.items.clear(); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (isSelectableConditionOrExtra(item)) { + this.items.add(item); + } + } + + return true; + } + @Override public String getWiredData() { - return null; + return WiredManager.getGson().toJson(new JsonData( + this.evaluationMode, + this.furniSource, + this.compareValue, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); } @Override public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.evaluationMode); + message.appendInt(this.furniSource); + message.appendInt(this.compareValue); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.evaluationMode = normalizeEvaluationMode(data.evaluationMode); + this.furniSource = normalizeFurniSource(data.furniSource); + this.compareValue = normalizeCompareValue(data.compareValue); + + if (data.itemIds != null) { + for (Integer itemId : data.itemIds) { + HabboItem item = room.getHabboItem(itemId); + + if (isSelectableConditionOrExtra(item)) { + this.items.add(item); + } + } + } + } + + return; + } + + String[] legacyData = wiredData.split("[;\t]"); + + try { + if (legacyData.length > 0) { + this.evaluationMode = normalizeEvaluationMode(Integer.parseInt(legacyData[0])); + } + + if (legacyData.length > 1) { + this.furniSource = normalizeFurniSource(Integer.parseInt(legacyData[1])); + } + + if (legacyData.length > 2) { + this.compareValue = normalizeCompareValue(Integer.parseInt(legacyData[2])); + } + } catch (NumberFormatException ignored) { + this.evaluationMode = MODE_ALL; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.compareValue = 1; + } } @Override public void onPickUp() { - + this.items.clear(); + this.evaluationMode = MODE_ALL; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.compareValue = 1; } @Override public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getEvaluationMode() { + return this.evaluationMode; + } + + public int getFurniSource() { + return this.furniSource; + } + + public int getCompareValue() { + return this.compareValue; + } + + private void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + HabboItem roomItem = room.getHabboItem(item.getId()); + + if (!isSelectableConditionOrExtra(roomItem)) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + public static boolean matchesMode(int evaluationMode, int matchedRequirements, int totalRequirements, int compareValue) { + if (totalRequirements <= 0) { + return true; + } + + switch (normalizeEvaluationMode(evaluationMode)) { + case MODE_AT_LEAST_ONE: + return matchedRequirements > 0; + case MODE_NOT_ALL: + return matchedRequirements > 0 && matchedRequirements < totalRequirements; + case MODE_NONE: + return matchedRequirements == 0; + case MODE_LESS_THAN: + return matchedRequirements < normalizeCompareValue(compareValue); + case MODE_EXACTLY: + return matchedRequirements == normalizeCompareValue(compareValue); + case MODE_MORE_THAN: + return matchedRequirements > normalizeCompareValue(compareValue); + case MODE_ALL: + default: + return matchedRequirements >= totalRequirements; + } + } + + private static int normalizeEvaluationMode(int value) { + switch (value) { + case MODE_ALL: + case MODE_AT_LEAST_ONE: + case MODE_NOT_ALL: + case MODE_NONE: + case MODE_LESS_THAN: + case MODE_EXACTLY: + case MODE_MORE_THAN: + return value; + default: + return MODE_ALL; + } + } + + private static int normalizeCompareValue(int value) { + return Math.max(MIN_COMPARE_VALUE, Math.min(MAX_COMPARE_VALUE, value)); + } + + private static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private static boolean isSelectableConditionOrExtra(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interaction = item.getBaseItem().getInteractionType().getName(); + if (interaction == null) { + return false; + } + + String normalizedInteraction = interaction.toLowerCase(); + return normalizedInteraction.startsWith("wf_cnd_") || normalizedInteraction.startsWith("wf_xtra_"); + } + + static class JsonData { + int evaluationMode; + int furniSource; + int compareValue; + List itemIds; + + JsonData(int evaluationMode, int furniSource, int compareValue, List itemIds) { + this.evaluationMode = evaluationMode; + this.furniSource = furniSource; + this.compareValue = compareValue; + this.itemIds = itemIds; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputUsername.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputUsername.java new file mode 100644 index 00000000..72fd5821 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputUsername.java @@ -0,0 +1,212 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.regex.Pattern; + +public class WiredExtraTextOutputUsername extends InteractionWiredExtra { + public static final int CODE = 67; + public static final int TYPE_SINGLE = 1; + public static final int TYPE_MULTIPLE = 2; + public static final String DEFAULT_PLACEHOLDER_NAME = ""; + public static final String DEFAULT_DELIMITER = ", "; + public static final int MAX_PLACEHOLDER_NAME_LENGTH = 32; + public static final int MAX_DELIMITER_LENGTH = 16; + + private static final Pattern WRAPPED_PLACEHOLDER_PATTERN = Pattern.compile("^\\$\\((.*)\\)$"); + + private String placeholderName = DEFAULT_PLACEHOLDER_NAME; + private int placeholderType = TYPE_SINGLE; + private String delimiter = DEFAULT_DELIMITER; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredExtraTextOutputUsername(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraTextOutputUsername(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] intParams = settings.getIntParams(); + String[] stringData = splitStringData(settings.getStringParam()); + + this.placeholderType = normalizePlaceholderType((intParams.length > 0) ? intParams[0] : TYPE_SINGLE); + this.userSource = normalizeUserSource((intParams.length > 1) ? intParams[1] : WiredSourceUtil.SOURCE_TRIGGER); + this.placeholderName = normalizePlaceholderName(stringData[0]); + this.delimiter = normalizeDelimiter(stringData[1]); + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.placeholderName, this.placeholderType, this.delimiter, this.userSource)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.placeholderName + "\t" + this.delimiter); + message.appendInt(2); + message.appendInt(this.placeholderType); + message.appendInt(this.userSource); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.placeholderName = normalizePlaceholderName(data.placeholderName); + this.placeholderType = normalizePlaceholderType(data.placeholderType); + this.delimiter = normalizeDelimiter(data.delimiter); + this.userSource = normalizeUserSource(data.userSource); + } + + return; + } + + String[] legacyData = splitStringData(wiredData); + this.placeholderName = normalizePlaceholderName(legacyData[0]); + this.delimiter = normalizeDelimiter(legacyData[1]); + } + + @Override + public void onPickUp() { + this.placeholderName = DEFAULT_PLACEHOLDER_NAME; + this.placeholderType = TYPE_SINGLE; + this.delimiter = DEFAULT_DELIMITER; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getPlaceholderName() { + return this.placeholderName; + } + + public String getPlaceholderToken() { + return this.placeholderName.isEmpty() ? "" : "$(" + this.placeholderName + ")"; + } + + public int getPlaceholderType() { + return this.placeholderType; + } + + public String getDelimiter() { + return this.delimiter; + } + + public int getUserSource() { + return this.userSource; + } + + private static String[] splitStringData(String value) { + if (value == null) { + return new String[] { DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER }; + } + + String[] parts = value.split("\t", -1); + + if (parts.length <= 1) { + return new String[] { value, DEFAULT_DELIMITER }; + } + + return new String[] { parts[0], parts[1] }; + } + + private static int normalizePlaceholderType(int value) { + return (value == TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE; + } + + private static String normalizePlaceholderName(String value) { + if (value == null) { + return DEFAULT_PLACEHOLDER_NAME; + } + + String normalized = value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + if (WRAPPED_PLACEHOLDER_PATTERN.matcher(normalized).matches()) { + normalized = normalized.substring(2, normalized.length() - 1).trim(); + } + + if (normalized.length() > MAX_PLACEHOLDER_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_PLACEHOLDER_NAME_LENGTH); + } + + return normalized; + } + + private static String normalizeDelimiter(String value) { + if (value == null) { + return DEFAULT_DELIMITER; + } + + String normalized = value.replace("\t", "").replace("\r", "").replace("\n", ""); + + if (normalized.length() > MAX_DELIMITER_LENGTH) { + normalized = normalized.substring(0, MAX_DELIMITER_LENGTH); + } + + return normalized; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + static class JsonData { + String placeholderName; + int placeholderType; + String delimiter; + int userSource; + + JsonData(String placeholderName, int placeholderType, String delimiter, int userSource) { + this.placeholderName = placeholderName; + this.placeholderType = placeholderType; + this.delimiter = delimiter; + this.userSource = userSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java index dae39207..6a2c537c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java @@ -313,21 +313,22 @@ public class RoomChatManager { } } + String wiredSayMessage = roomChatMessage.getMessage(); + // Handle commands and wired + boolean suppressSaysOutput = false; if (chatType != RoomChatType.WHISPER) { if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) { - WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), roomChatMessage.getMessage()); + WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage); roomChatMessage.isCommand = true; return; } if (!ignoreWired) { - if (WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), roomChatMessage.getMessage())) { - habbo.getClient().sendResponse(new RoomUserWhisperComposer( - new RoomChatMessage(roomChatMessage.getMessage(), habbo, habbo, - roomChatMessage.getBubble()))); - return; - } + suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput( + habbo.getHabboInfo().getCurrentRoom(), + habbo.getRoomUnit(), + wiredSayMessage); } } @@ -388,9 +389,25 @@ public class RoomChatManager { if (chatType == RoomChatType.WHISPER) { this.handleWhisper(habbo, roomChatMessage, prefixMessage, clearPrefixMessage); } else if (chatType == RoomChatType.TALK) { - this.handleTalk(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + if (suppressSaysOutput) { + habbo.getClient().sendResponse(new RoomUserWhisperComposer( + new RoomChatMessage(roomChatMessage.getMessage(), habbo, habbo, + roomChatMessage.getBubble()))); + } else { + this.handleTalk(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + } } else if (chatType == RoomChatType.SHOUT) { - this.handleShout(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + if (suppressSaysOutput) { + habbo.getClient().sendResponse(new RoomUserWhisperComposer( + new RoomChatMessage(roomChatMessage.getMessage(), habbo, habbo, + roomChatMessage.getBubble()))); + } else { + this.handleShout(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + } + } + + if (chatType != RoomChatType.WHISPER && !ignoreWired && !roomChatMessage.isCommand) { + WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage); } // Notify bots and talking furniture diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java index 759f5b86..5e81da8d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java @@ -12,6 +12,7 @@ import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveR import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectTriggerStacks; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; import com.eu.habbo.habbohotel.rooms.Room; @@ -40,8 +41,10 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; public class WiredHandler { private static final Logger LOGGER = LoggerFactory.getLogger(WiredHandler.class); @@ -185,20 +188,24 @@ public class WiredHandler { } if (!conditions.isEmpty()) { - ArrayList matchedConditions = new ArrayList<>(conditions.size()); - for (InteractionWiredCondition searchMatched : conditions) { - if (!matchedConditions.contains(searchMatched.getType()) && searchMatched.operator() == WiredConditionOperator.OR && searchMatched.execute(roomUnit, room, stuff)) { - matchedConditions.add(searchMatched.getType()); + int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL; + int conditionEvaluationValue = 1; + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraOrEval) { + conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode(); + conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue(); + break; } } - for (InteractionWiredCondition condition : conditions) { - if (!((condition.operator() == WiredConditionOperator.OR && matchedConditions.contains(condition.getType())) || - (condition.operator() == WiredConditionOperator.AND && condition.execute(roomUnit, room, stuff))) && - !Emulator.getPluginManager().fireEvent(new WiredConditionFailedEvent(room, roomUnit, trigger, condition)).isCancelled()) { - - return false; + if (!evaluateConditions(conditions, roomUnit, room, stuff, conditionEvaluationMode, conditionEvaluationValue)) { + for (InteractionWiredCondition condition : conditions) { + if (!Emulator.getPluginManager().fireEvent(new WiredConditionFailedEvent(room, roomUnit, trigger, condition)).isCancelled()) { + break; + } } + + return false; } } @@ -253,6 +260,40 @@ public class WiredHandler { return false; } + private static boolean evaluateConditions(THashSet conditions, RoomUnit roomUnit, Room room, Object[] stuff, int evaluationMode, int evaluationValue) { + if (conditions == null || conditions.isEmpty()) { + return true; + } + + Map orGroupResults = new HashMap<>(); + int matchedRequirements = 0; + int totalRequirements = 0; + + for (InteractionWiredCondition condition : conditions) { + boolean result = condition.execute(roomUnit, room, stuff); + + if (condition.operator() == WiredConditionOperator.OR) { + orGroupResults.merge(condition.getType(), result, (left, right) -> left || right); + continue; + } + + totalRequirements++; + if (result) { + matchedRequirements++; + } + } + + totalRequirements += orGroupResults.size(); + + for (Boolean groupResult : orGroupResults.values()) { + if (Boolean.TRUE.equals(groupResult)) { + matchedRequirements++; + } + } + + return WiredExtraOrEval.matchesMode(evaluationMode, matchedRequirements, totalRequirements, evaluationValue); + } + private static boolean triggerEffect(InteractionWiredEffect effect, RoomUnit roomUnit, Room room, Object[] stuff, long millis) { boolean executed = false; if (effect != null && (effect.canExecute(millis) || (roomUnit != null && effect.requiresTriggeringUser() && Emulator.getConfig().getBoolean("wired.custom.enabled", false) && effect.userCanExecute(roomUnit.getId(), millis)))) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java index 9833f53f..337e0734 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java @@ -36,7 +36,8 @@ public final class WiredStack { private final List effects; // Extra modifiers - private final boolean useOrMode; // WiredExtraOrEval present + private final int conditionEvaluationMode; // WiredExtraOrEval mode + private final int conditionEvaluationValue; // WiredExtraOrEval numeric threshold private final boolean useRandom; // WiredExtraRandom present private final boolean useUnseen; // WiredExtraUnseen present private final boolean executeInOrder; // WiredExtraExecuteInOrder present @@ -53,7 +54,7 @@ public final class WiredStack { IWiredTrigger trigger, List conditions, List effects) { - this(triggerItem, trigger, conditions, effects, false, false, false, false); + this(triggerItem, trigger, conditions, effects, 0, 1, false, false, false); } /** @@ -63,7 +64,8 @@ public final class WiredStack { * @param trigger the trigger implementation * @param conditions list of conditions * @param effects list of effects - * @param useOrMode if true, conditions use OR logic (any pass = success) + * @param conditionEvaluationMode condition evaluation mode from WiredExtraOrEval + * @param conditionEvaluationValue numeric comparison value from WiredExtraOrEval * @param useRandom if true, select one random effect instead of all * @param useUnseen if true, execute effects in "unseen" order (round-robin) * @param executeInOrder if true, execute all regular effects in stable stack order @@ -72,7 +74,8 @@ public final class WiredStack { IWiredTrigger trigger, List conditions, List effects, - boolean useOrMode, + int conditionEvaluationMode, + int conditionEvaluationValue, boolean useRandom, boolean useUnseen, boolean executeInOrder) { @@ -80,7 +83,8 @@ public final class WiredStack { this.trigger = trigger; this.conditions = conditions != null ? Collections.unmodifiableList(conditions) : Collections.emptyList(); this.effects = effects != null ? Collections.unmodifiableList(effects) : Collections.emptyList(); - this.useOrMode = useOrMode; + this.conditionEvaluationMode = conditionEvaluationMode; + this.conditionEvaluationValue = conditionEvaluationValue; this.useRandom = useRandom; this.useUnseen = useUnseen; this.executeInOrder = executeInOrder; @@ -135,12 +139,19 @@ public final class WiredStack { } /** - * Check if OR mode is enabled (WiredExtraOrEval). - * When true, any condition passing means all pass. - * @return true if OR mode is enabled + * Get the condition evaluation mode from WiredExtraOrEval. + * @return evaluation mode code */ - public boolean useOrMode() { - return useOrMode; + public int conditionEvaluationMode() { + return conditionEvaluationMode; + } + + /** + * Get the condition evaluation numeric value from WiredExtraOrEval. + * @return comparison value + */ + public int conditionEvaluationValue() { + return conditionEvaluationValue; } /** @@ -193,7 +204,8 @@ public final class WiredStack { ", trigger=" + (trigger != null ? trigger.listensTo() : "null") + ", conditions=" + conditions.size() + ", effects=" + effects.size() + - ", orMode=" + useOrMode + + ", conditionEvaluationMode=" + conditionEvaluationMode + + ", conditionEvaluationValue=" + conditionEvaluationValue + ", random=" + useRandom + ", unseen=" + useUnseen + ", executeInOrder=" + executeInOrder + diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java index d67c7b82..5be6f490 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java @@ -2,6 +2,7 @@ package com.eu.habbo.habbohotel.wired.core; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; @@ -174,17 +175,30 @@ public final class RoomWiredStackIndex implements WiredStackIndex { List effects = collectEffects(rawEffects); // Check for extras - boolean useOrMode = specialTypes.hasExtraType(x, y, WiredExtraOrEval.class); + THashSet extras = specialTypes.getExtras(x, y); + int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL; + int conditionEvaluationValue = 1; boolean useRandom = specialTypes.hasExtraType(x, y, WiredExtraRandom.class); boolean useUnseen = specialTypes.hasExtraType(x, y, WiredExtraUnseen.class); boolean executeInOrder = specialTypes.hasExtraType(x, y, WiredExtraExecuteInOrder.class); + if (extras != null) { + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraOrEval) { + conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode(); + conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue(); + break; + } + } + } + return new WiredStack( trigger, wrappedTrigger, conditions, effects, - useOrMode, + conditionEvaluationMode, + conditionEvaluationValue, useRandom, useUnseen, executeInOrder 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 5d039ba6..db624a5a 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 @@ -7,6 +7,7 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; @@ -305,83 +306,102 @@ public final class WiredEngine { return true; } + private boolean wouldTriggerStack(WiredStack stack, WiredEvent event, long currentTime) { + Room room = event.getRoom(); + + if (!stack.trigger().matches(stack.triggerItem(), event)) { + return false; + } + + if (stack.trigger().requiresActor() && !event.getActor().isPresent()) { + return false; + } + + WiredState state = new WiredState(maxStepsPerStack); + WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + + state.step(); + + List executedSelectors = Collections.emptyList(); + if (stack.hasEffects()) { + executedSelectors = executeSelectors(stack, ctx); + applySelectionFilterExtras(stack, ctx, executedSelectors); + } + + if (stack.hasConditions() && !evaluateConditions(stack, ctx)) { + return false; + } + + WiredExtraExecutionLimit executionLimitExtra = getExecutionLimitExtra(room, stack); + return executionLimitExtra == null || executionLimitExtra.canExecuteAt(currentTime); + } + /** * Evaluate all conditions in a stack. */ private boolean evaluateConditions(WiredStack stack, WiredContext ctx) { List conditions = stack.conditions(); - - if (stack.useOrMode()) { - // OR mode: at least one condition must pass - return evaluateOrMode(conditions, ctx); - } else { - // Standard mode: use individual operators - return evaluateStandardMode(conditions, ctx); - } + + return evaluateConditionsByMode(conditions, ctx, stack.conditionEvaluationMode(), stack.conditionEvaluationValue()); } /** - * Evaluate conditions in OR mode (any pass = success). + * Evaluate conditions according to the configured stack mode. */ - private boolean evaluateOrMode(List conditions, WiredContext ctx) { - // Group by condition type (for legacy compatibility) - Map typeResults = new HashMap<>(); - + private boolean evaluateConditionsByMode(List conditions, WiredContext ctx, int evaluationMode, int evaluationValue) { + if (conditions == null || conditions.isEmpty()) { + return true; + } + + Room room = ctx.room(); + Map> groupedOrResults = new LinkedHashMap<>(); + int matchedRequirements = 0; + int totalRequirements = 0; + for (IWiredCondition condition : conditions) { ctx.state().step(); - - String typeName = condition.getClass().getSimpleName(); - if (!typeResults.containsKey(typeName) && condition.evaluate(ctx)) { - typeResults.put(typeName, true); + + boolean result = condition.evaluate(ctx); + String conditionKey = getConditionGroupKey(condition); + + if (condition.operator() == WiredConditionOperator.OR) { + groupedOrResults.computeIfAbsent(conditionKey, ignored -> new ArrayList<>()).add(result); + debug(room, " Condition (OR group {}) {}: {}", conditionKey, condition.getClass().getSimpleName(), result ? "PASS" : "FAIL"); + continue; } + + totalRequirements++; + + if (result) { + matchedRequirements++; + } + + debug(room, " Condition {}: {}", condition.getClass().getSimpleName(), result ? "PASS" : "FAIL"); } - - // At least one condition type must have passed - return !typeResults.isEmpty(); + + for (Map.Entry> entry : groupedOrResults.entrySet()) { + totalRequirements++; + + boolean groupPassed = entry.getValue().stream().anyMatch(Boolean::booleanValue); + if (groupPassed) { + matchedRequirements++; + } + + debug(room, " Condition (OR result {}) : {}", entry.getKey(), groupPassed ? "PASS" : "FAIL"); + } + + boolean matches = WiredExtraOrEval.matchesMode(evaluationMode, matchedRequirements, totalRequirements, evaluationValue); + + debug(room, "Condition eval mode {} value {} matched {}/{} logical requirements => {}", evaluationMode, evaluationValue, matchedRequirements, totalRequirements, matches ? "PASS" : "FAIL"); + return matches; } - /** - * Evaluate conditions in standard mode using operators. - */ - private boolean evaluateStandardMode(List conditions, WiredContext ctx) { - Room room = ctx.room(); - - // First pass: collect all OR conditions that passed - Map orResults = new HashMap<>(); - for (IWiredCondition condition : conditions) { - if (condition.operator() == WiredConditionOperator.OR) { - ctx.state().step(); - String typeName = condition.getClass().getSimpleName(); - boolean result = condition.evaluate(ctx); - debug(room, " Condition (OR) {}: {}", typeName, result ? "PASS" : "FAIL"); - if (!orResults.containsKey(typeName) && result) { - orResults.put(typeName, true); - } - } + private String getConditionGroupKey(IWiredCondition condition) { + if (condition instanceof InteractionWiredCondition) { + return String.valueOf(((InteractionWiredCondition) condition).getType()); } - // Second pass: verify all conditions - for (IWiredCondition condition : conditions) { - boolean passes; - String typeName = condition.getClass().getSimpleName(); - - if (condition.operator() == WiredConditionOperator.OR) { - // OR: passes if any of same type passed - passes = orResults.containsKey(typeName); - debug(room, " Condition (OR check) {}: {}", typeName, passes ? "PASS" : "FAIL"); - } else { - // AND: must evaluate and pass - ctx.state().step(); - passes = condition.evaluate(ctx); - debug(room, " Condition (AND) {}: {}", typeName, passes ? "PASS" : "FAIL"); - } - - if (!passes) { - return false; - } - } - - return true; + return condition.getClass().getName(); } /** @@ -646,6 +666,51 @@ public final class WiredEngine { } } + /** + * Preview whether a USER_SAYS event should suppress the public room chat output. + * This mirrors trigger and condition eligibility without executing regular effects. + */ + public boolean shouldSuppressUserSaysOutput(WiredEvent event) { + if (event == null || event.getType() != WiredEvent.Type.USER_SAYS) { + return false; + } + + Room room = event.getRoom(); + if (room == null || !room.isLoaded()) { + return false; + } + + List stacks = index.getStacks(room, event.getType()); + if (stacks.isEmpty()) { + return false; + } + + long triggerTime = event.getCreatedAtMs(); + + for (WiredStack stack : stacks) { + if (!(stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword)) { + continue; + } + + WiredTriggerHabboSaysKeyword trigger = (WiredTriggerHabboSaysKeyword) stack.triggerItem(); + if (!trigger.isHideMessage()) { + continue; + } + + try { + if (wouldTriggerStack(stack, event, triggerTime)) { + return true; + } + } catch (WiredLimitException limitEx) { + debug(room, "Suppression preview stopped (limit): {}", limitEx.getMessage()); + } catch (Exception ex) { + LOGGER.warn("Error previewing USER_SAYS suppression in room {}: {}", room.getId(), ex.getMessage()); + } + } + + return false; + } + private void scheduleOrderedEffectBatch(List batch, WiredContext ctx, int delay, long triggerTime) { long delayMs = delay * 500L; long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - triggerTime); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java index 0e15a5e0..a3077406 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java @@ -316,6 +316,15 @@ public final class WiredManager { return handleEvent(event); } + public static boolean shouldSuppressUserSaysOutput(Room room, RoomUnit user, String message) { + if (!isEnabled() || engine == null || room == null || user == null) { + return false; + } + + WiredEvent event = WiredEvents.userSays(room, user, message); + return engine.shouldSuppressUserSaysOutput(event); + } + /** * Trigger when a user enters the room. */ 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 new file mode 100644 index 00000000..6fb80e11 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java @@ -0,0 +1,113 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputUsername; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +public final class WiredTextPlaceholderUtil { + private WiredTextPlaceholderUtil() { + } + + public static String applyUsernamePlaceholders(WiredContext ctx, String text) { + if (ctx == null || text == null || text.isEmpty()) { + return text; + } + + Room room = ctx.room(); + HabboItem triggerItem = ctx.triggerItem(); + + if (room == null || triggerItem == null || room.getRoomSpecialTypes() == null) { + return text; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); + if (extras == null || extras.isEmpty()) { + return text; + } + + String resolvedText = text; + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (!(extra instanceof WiredExtraTextOutputUsername)) { + continue; + } + + WiredExtraTextOutputUsername usernameExtra = (WiredExtraTextOutputUsername) extra; + String placeholderToken = usernameExtra.getPlaceholderToken(); + + if (placeholderToken.isEmpty() || !resolvedText.contains(placeholderToken)) { + continue; + } + + resolvedText = resolvedText.replace(placeholderToken, buildUsernameReplacement(ctx, usernameExtra)); + } + + return resolvedText; + } + + public static boolean requiresActor(Room room, HabboItem stackItem) { + if (room == null || stackItem == null || room.getRoomSpecialTypes() == null) { + return false; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(stackItem.getX(), stackItem.getY()); + if (extras == null || extras.isEmpty()) { + return false; + } + + for (InteractionWiredExtra extra : extras) { + if (!(extra instanceof WiredExtraTextOutputUsername)) { + continue; + } + + int userSource = ((WiredExtraTextOutputUsername) extra).getUserSource(); + if ((userSource == WiredSourceUtil.SOURCE_TRIGGER) || (userSource == WiredSourceUtil.SOURCE_CLICKED_USER)) { + return true; + } + } + + return false; + } + + private static String buildUsernameReplacement(WiredContext ctx, WiredExtraTextOutputUsername extra) { + List users = WiredSourceUtil.resolveUsers(ctx, extra.getUserSource()); + if (users.isEmpty()) { + return ""; + } + + Room room = ctx.room(); + LinkedHashSet seenUserIds = new LinkedHashSet<>(); + List usernames = new ArrayList<>(); + + for (RoomUnit unit : users) { + if ((unit == null) || !seenUserIds.add(unit.getId())) { + continue; + } + + Habbo habbo = room.getHabbo(unit); + if ((habbo == null) || (habbo.getHabboInfo() == null)) { + continue; + } + + usernames.add(habbo.getHabboInfo().getUsername()); + } + + if (usernames.isEmpty()) { + return ""; + } + + if (extra.getPlaceholderType() == WiredExtraTextOutputUsername.TYPE_MULTIPLE) { + return String.join(extra.getDelimiter(), usernames); + } + + return usernames.get(0); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java index c0b5dfe3..5a902bfe 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java @@ -11,8 +11,19 @@ import gnu.trove.procedure.TIntProcedure; import java.util.ArrayList; public class UserClothesComposer extends MessageComposer { + private static class ClothEntry { + private final String name; + private final int[] setIds; + + private ClothEntry(String name, int[] setIds) { + this.name = name; + this.setIds = setIds; + } + } + private final ArrayList idList = new ArrayList<>(); private final ArrayList nameList = new ArrayList<>(); + private final ArrayList clothEntries = new ArrayList<>(); public UserClothesComposer(Habbo habbo) { habbo.getInventory().getWardrobeComponent().getClothing().forEach(new TIntProcedure() { @@ -31,6 +42,12 @@ public class UserClothesComposer extends MessageComposer { return true; } }); + + for (ClothItem item : Emulator.getGameEnvironment().getCatalogManager().clothing.values()) { + if (item != null) { + this.clothEntries.add(new ClothEntry(item.name, item.setId)); + } + } } @Override @@ -40,6 +57,17 @@ public class UserClothesComposer extends MessageComposer { this.idList.forEach(this.response::appendInt); this.response.appendInt(this.nameList.size()); this.nameList.forEach(this.response::appendString); + this.response.appendInt(this.clothEntries.size()); + + for (ClothEntry entry : this.clothEntries) { + this.response.appendString(entry.name); + this.response.appendInt(entry.setIds.length); + + for (int setId : entry.setIds) { + this.response.appendInt(setId); + } + } + return this.response; }