From 264bea3c8b395bbf2ea58b1064b189bea4a1c25f Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Mon, 16 Mar 2026 15:12:42 +0100 Subject: [PATCH] feat(wired): add antenna signals and selector-aware snapshots --- .../habbo/habbohotel/items/ItemManager.java | 1 + .../WiredConditionMatchStatePosition.java | 113 ++++---- .../wired/effects/WiredEffectMatchFurni.java | 139 ++++++---- .../wired/effects/WiredEffectSendSignal.java | 255 +++++++++++++----- .../selector/WiredEffectFurniByType.java | 18 +- .../triggers/WiredTriggerReceiveSignal.java | 119 +++++++- .../habbohotel/wired/core/WiredEngine.java | 50 ++-- .../wired/WiredTriggerSaveDataEvent.java | 22 +- .../wired/WiredTriggerSaveException.java | 7 + 9 files changed, 503 insertions(+), 221 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveException.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 50928f05..2a944f95 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 @@ -202,6 +202,7 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("random_state", InteractionRandomState.class)); this.interactionsList.add(new ItemInteraction("vendingmachine_no_sides", InteractionNoSidesVendingMachine.class)); this.interactionsList.add(new ItemInteraction("tile_walkmagic", InteractionTileWalkMagic.class)); + this.interactionsList.add(new ItemInteraction("antenna", InteractionDefault.class)); this.interactionsList.add(new ItemInteraction("game_timer", InteractionGameTimer.class)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java index bc965875..7966d563 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java @@ -19,9 +19,7 @@ import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; public class WiredConditionMatchStatePosition extends InteractionWiredCondition implements InteractionWiredMatchFurniSettings { public static final WiredConditionType type = WiredConditionType.MATCH_SSHOT; @@ -92,14 +90,12 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.settings.clear(); - if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - for (int i = 0; i < count; i++) { - int itemId = settings.getFurniIds()[i]; - HabboItem item = room.getHabboItem(itemId); + for (int i = 0; i < count; i++) { + int itemId = settings.getFurniIds()[i]; + HabboItem item = room.getHabboItem(itemId); - if (item != null) - this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY())); - } + if (item != null) + this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY())); } return true; @@ -108,65 +104,71 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition @Override public boolean evaluate(WiredContext ctx) { Room room = ctx.room(); + this.refresh(); + if (this.settings.isEmpty()) return true; - List targets = null; - Set targetIds = null; - if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { - targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, null); + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, null); if (targets.isEmpty()) return false; - targetIds = new HashSet<>(); - for (HabboItem item : targets) { - if (item != null) targetIds.add(item.getId()); - } - if (targetIds.isEmpty()) return false; - } - THashSet toRemove = new THashSet<>(); - Set settingsIds = new HashSet<>(); + for (HabboItem item : targets) { + if (item == null) return false; + + WiredMatchFurniSetting setting = this.resolveSettingForTarget(room, item); + if (setting == null) { + return false; + } + + if (!this.matchesSetting(item, setting)) { + return false; + } + } + + return true; + } for (WiredMatchFurniSetting setting : this.settings) { - if (targetIds != null && !targetIds.contains(setting.item_id)) { - continue; - } HabboItem item = room.getHabboItem(setting.item_id); - - if (item != null) { - settingsIds.add(setting.item_id); - if (this.state) { - if (!item.getExtradata().equals(setting.state)) - return false; - } - - if (this.position) { - if (!(setting.x == item.getX() && setting.y == item.getY())) - return false; - } - - if (this.direction) { - if (setting.rotation != item.getRotation()) - return false; - } - } else { - toRemove.add(setting); - } - } - - if (targetIds != null && !settingsIds.containsAll(targetIds)) { - return false; - } - - if (!toRemove.isEmpty()) { - for (WiredMatchFurniSetting setting : toRemove) { - this.settings.remove(setting); - } + if (item == null) continue; + if (!this.matchesSetting(item, setting)) + return false; } return true; } + private WiredMatchFurniSetting resolveSettingForTarget(Room room, HabboItem target) { + WiredMatchFurniSetting fallback = null; + + for (WiredMatchFurniSetting setting : this.settings) { + HabboItem sourceItem = room.getHabboItem(setting.item_id); + if (sourceItem == null) continue; + if (sourceItem.getBaseItem().getId() != target.getBaseItem().getId()) continue; + + if (setting.state.equals(target.getExtradata())) { + return setting; + } + + if (fallback == null) { + fallback = setting; + } + } + + return fallback; + } + + private boolean matchesSetting(HabboItem item, WiredMatchFurniSetting setting) { + if (this.state && !item.getExtradata().equals(setting.state)) + return false; + + if (this.position && !(setting.x == item.getX() && setting.y == item.getY())) + return false; + + return !this.direction || setting.rotation == item.getRotation(); + } + @Deprecated @Override public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { @@ -214,9 +216,6 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.position = data[4].equals("1"); this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.settings.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } } @Override diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java index bab1896a..90348f35 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java @@ -50,6 +50,7 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int @Override public void execute(WiredContext ctx) { Room room = ctx.room(); + this.refresh(); if(this.settings.isEmpty()) return; @@ -57,54 +58,80 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int if (room.getLayout() == null) return; - java.util.Set allowedItemIds = null; - if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { - allowedItemIds = new java.util.HashSet<>(); - for (HabboItem si : WiredSourceUtil.resolveItems(ctx, this.furniSource, null)) { - if (si != null) { - allowedItemIds.add(si.getId()); + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (WiredMatchFurniSetting setting : this.settings) { + HabboItem item = room.getHabboItem(setting.item_id); + if (item != null) { + this.applySetting(room, item, setting); } } - if (allowedItemIds.isEmpty()) { - return; + + return; + } + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, null); + if (targets.isEmpty()) { + return; + } + + for (HabboItem item : targets) { + if (item == null) continue; + + WiredMatchFurniSetting setting = this.resolveSettingForTarget(room, item); + if (setting == null) continue; + + this.applySetting(room, item, setting); + } + } + + private WiredMatchFurniSetting resolveSettingForTarget(Room room, HabboItem target) { + WiredMatchFurniSetting fallback = null; + + for (WiredMatchFurniSetting setting : this.settings) { + HabboItem sourceItem = room.getHabboItem(setting.item_id); + if (sourceItem == null) continue; + if (sourceItem.getBaseItem().getId() != target.getBaseItem().getId()) continue; + + if (setting.state.equals(target.getExtradata())) { + return setting; + } + + if (fallback == null) { + fallback = setting; } } - for (WiredMatchFurniSetting setting : this.settings) { - if (allowedItemIds != null && !allowedItemIds.contains(setting.item_id)) continue; + return fallback; + } - HabboItem item = room.getHabboItem(setting.item_id); - if (item != null) { - if (this.state && (this.checkForWiredResetPermission && item.allowWiredResetState())) { - if (!setting.state.equals(" ") && !item.getExtradata().equals(setting.state)) { - item.setExtradata(setting.state); - room.updateItemState(item); + private void applySetting(Room room, HabboItem item, WiredMatchFurniSetting setting) { + if (this.state && (this.checkForWiredResetPermission && item.allowWiredResetState())) { + if (!setting.state.equals(" ") && !item.getExtradata().equals(setting.state)) { + item.setExtradata(setting.state); + room.updateItemState(item); + } + } + + RoomTile oldLocation = room.getLayout().getTile(item.getX(), item.getY()); + if (oldLocation == null) return; + double oldZ = item.getZ(); + + if(this.direction && !this.position) { + if(item.getRotation() != setting.rotation && room.furnitureFitsAt(oldLocation, item, setting.rotation, false) == FurnitureMovementError.NONE) { + room.moveFurniTo(item, oldLocation, setting.rotation, null, true); + } + } + else if(this.position) { + boolean slideAnimation = !this.direction || item.getRotation() == setting.rotation; + RoomTile newLocation = room.getLayout().getTile((short) setting.x, (short) setting.y); + int newRotation = this.direction ? setting.rotation : item.getRotation(); + + if(newLocation != null && newLocation.state != RoomTileState.INVALID && (newLocation != oldLocation || newRotation != item.getRotation()) && room.furnitureFitsAt(newLocation, item, newRotation, true) == FurnitureMovementError.NONE) { + if(room.moveFurniTo(item, newLocation, newRotation, null, !slideAnimation) == FurnitureMovementError.NONE) { + if(slideAnimation) { + room.sendComposer(new FloorItemOnRollerComposer(item, null, oldLocation, oldZ, newLocation, item.getZ(), 0, room).compose()); } } - - RoomTile oldLocation = room.getLayout().getTile(item.getX(), item.getY()); - if (oldLocation == null) continue; - double oldZ = item.getZ(); - - if(this.direction && !this.position) { - if(item.getRotation() != setting.rotation && room.furnitureFitsAt(oldLocation, item, setting.rotation, false) == FurnitureMovementError.NONE) { - room.moveFurniTo(item, oldLocation, setting.rotation, null, true); - } - } - else if(this.position) { - boolean slideAnimation = !this.direction || item.getRotation() == setting.rotation; - RoomTile newLocation = room.getLayout().getTile((short) setting.x, (short) setting.y); - int newRotation = this.direction ? setting.rotation : item.getRotation(); - - if(newLocation != null && newLocation.state != RoomTileState.INVALID && (newLocation != oldLocation || newRotation != item.getRotation()) && room.furnitureFitsAt(newLocation, item, newRotation, true) == FurnitureMovementError.NONE) { - if(room.moveFurniTo(item, newLocation, newRotation, null, !slideAnimation) == FurnitureMovementError.NONE) { - if(slideAnimation) { - room.sendComposer(new FloorItemOnRollerComposer(item, null, oldLocation, oldZ, newLocation, item.getZ(), 0, room).compose()); - } - } - } - } - } } } @@ -134,9 +161,6 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int this.settings.clear(); this.settings.addAll(data.items); this.furniSource = data.furniSource; - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.settings.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } } else { String[] data = set.getString("wired_data").split(":"); @@ -221,23 +245,22 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int if (room == null) throw new WiredSaveException("Trying to save wired in unloaded room"); + int itemsCount = settings.getFurniIds().length; + + if(itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + List newSettings = new ArrayList<>(); - if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - int itemsCount = settings.getFurniIds().length; - if(itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { - throw new WiredSaveException("Too many furni selected"); - } + for (int i = 0; i < itemsCount; i++) { + int itemId = settings.getFurniIds()[i]; + HabboItem it = room.getHabboItem(itemId); - for (int i = 0; i < itemsCount; i++) { - int itemId = settings.getFurniIds()[i]; - HabboItem it = room.getHabboItem(itemId); + if(it == null) + throw new WiredSaveException(String.format("Item %s not found", itemId)); - if(it == null) - throw new WiredSaveException(String.format("Item %s not found", itemId)); - - newSettings.add(new WiredMatchFurniSetting(it.getId(), this.checkForWiredResetPermission && it.allowWiredResetState() ? it.getExtradata() : " ", it.getRotation(), it.getX(), it.getY())); - } + newSettings.add(new WiredMatchFurniSetting(it.getId(), this.checkForWiredResetPermission && it.allowWiredResetState() ? it.getExtradata() : " ", it.getRotation(), it.getX(), it.getY())); } int delay = settings.getDelay(); @@ -249,9 +272,7 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int this.direction = setDirection; this.position = setPosition; this.settings.clear(); - if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - this.settings.addAll(newSettings); - } + this.settings.addAll(newSettings); this.setDelay(delay); return true; 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 66a4429d..82bdd99c 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 @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -36,14 +37,16 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { private static final int ANTENNA_PICKED = 0; private static final int ANTENNA_TRIGGER = 1; - - private static final int FORWARD_NONE = 0; - private static final int FORWARD_TRIGGER = 1; + private static final String ANTENNA_INTERACTION = "antenna"; + private static final String FORWARD_ITEM_SPLIT_REGEX = "[;,\\t]"; + private static final long ANTENNA_PULSE_MS = 300L; + private static final ConcurrentHashMap ANTENNA_PULSE_TOKENS = new ConcurrentHashMap<>(); private THashSet items; + private THashSet forwardItems; private int antennaSource = ANTENNA_PICKED; - private int furniForward = FORWARD_NONE; - private int userForward = FORWARD_NONE; + private int furniForward = WiredSourceUtil.SOURCE_TRIGGER; + private int userForward = WiredSourceUtil.SOURCE_TRIGGER; private boolean signalPerFurni = false; private boolean signalPerUser = false; private int channel = 0; @@ -51,11 +54,13 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { public WiredEffectSendSignal(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); this.items = new THashSet<>(); + this.forwardItems = new THashSet<>(); } public WiredEffectSendSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); this.items = new THashSet<>(); + this.forwardItems = new THashSet<>(); } @Override @@ -77,73 +82,68 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .map(Collections::singleton) .orElse(Collections.emptySet()); } else { - antennas = ctx.targets().isItemsModifiedBySelector() - ? new ArrayList<>(ctx.targets().items()) - : new ArrayList<>(this.items); + Collection baseAntennas = new ArrayList<>(this.items); + + if (baseAntennas.isEmpty() && antennaSource > ANTENNA_TRIGGER) { + HabboItem antenna = room.getHabboItem(antennaSource); + antennas = (antenna != null) ? Collections.singleton(antenna) : Collections.emptySet(); + } else { + antennas = baseAntennas; + } } - if (antennas.isEmpty()) { + List resolvedAntennas = antennas.stream() + .filter(Objects::nonNull) + .filter(this::isAntennaItem) + .collect(Collectors.toList()); + + if (resolvedAntennas.isEmpty()) { LOGGER.debug("[SendSignal] No antennas resolved, aborting. antennaSource={}, selectorModified={}", antennaSource, ctx.targets().isItemsModifiedBySelector()); return; } - LOGGER.debug("[SendSignal] Resolved {} antenna(s), firing signals", antennas.size()); + LOGGER.debug("[SendSignal] Resolved {} antenna(s), firing signals", resolvedAntennas.size()); - RoomUnit forwardedUser = null; - if (userForward == FORWARD_TRIGGER) { - forwardedUser = ctx.actor().orElse(null); - } + List forwardedUsers = WiredSourceUtil.resolveUsers(ctx, this.userForward); + List forwardedFurni = WiredSourceUtil.resolveItems(ctx, this.furniForward, this.forwardItems); - HabboItem forwardedFurni = null; - if (furniForward == FORWARD_TRIGGER) { - forwardedFurni = ctx.sourceItem().orElse(null); - } + RoomUnit defaultUser = forwardedUsers.isEmpty() ? null : forwardedUsers.get(0); + HabboItem defaultFurni = forwardedFurni.isEmpty() ? null : forwardedFurni.get(0); - Set visitedTiles = new HashSet<>(); - List antennaTiles = new ArrayList<>(); - for (HabboItem antenna : antennas) { - if (antenna == null) continue; - String key = antenna.getX() + "," + antenna.getY(); - if (visitedTiles.add(key)) { - RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); - if (tile != null) { - antennaTiles.add(tile); - } - } - } + Collection usersToSend = (signalPerUser && !forwardedUsers.isEmpty()) + ? forwardedUsers + : Collections.singletonList(defaultUser); + + Collection furniToSend = (signalPerFurni && !forwardedFurni.isEmpty()) + ? forwardedFurni + : Collections.singletonList(defaultFurni); int nextDepth = currentDepth + 1; - if (signalPerFurni || signalPerUser) { - if (signalPerFurni) { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, forwardedUser, forwardedFurni, nextDepth); + for (RoomUnit user : usersToSend) { + for (HabboItem sourceItem : furniToSend) { + for (HabboItem antenna : resolvedAntennas) { + fireSignalAtAntenna(room, antenna, user, sourceItem, nextDepth); } } - if (signalPerUser && ctx.targets().hasUsers()) { - for (RoomUnit user : ctx.targets().users()) { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, user, forwardedFurni, nextDepth); - } - } - } else if (!signalPerFurni) { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, forwardedUser, forwardedFurni, nextDepth); - } - } - } else { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, forwardedUser, forwardedFurni, nextDepth); - } } } - private void fireSignalAtTile(Room room, RoomTile tile, RoomUnit actor, HabboItem sourceItem, int depth) { - LOGGER.debug("[SendSignal] fireSignalAtTile: tile={},{} depth={} channel={} actor={} sourceItem={}", tile.x, tile.y, depth, channel, actor != null ? actor.getId() : "null", sourceItem != null ? sourceItem.getId() : "null"); + private void fireSignalAtAntenna(Room room, HabboItem antenna, RoomUnit actor, HabboItem sourceItem, int depth) { + if (antenna == null) return; + RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); + if (tile == null) return; + + pulseAntenna(room, antenna); + + int signalChannel = antenna.getId(); + + LOGGER.debug("[SendSignal] fireSignalAtAntenna: antennaId={} tile={},{} depth={} channel={} actor={} sourceItem={}", + signalChannel, tile.x, tile.y, depth, signalChannel, actor != null ? actor.getId() : "null", sourceItem != null ? sourceItem.getId() : "null"); WiredEvent.Builder builder = WiredEvent.builder(WiredEvent.Type.SIGNAL_RECEIVED, room) .tile(tile) .callStackDepth(depth) - .signalChannel(this.channel) + .signalChannel(signalChannel) .triggeredByEffect(true); if (actor != null) builder.actor(actor); @@ -153,6 +153,33 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { LOGGER.debug("[SendSignal] handleEvent returned: {}", result); } + private void pulseAntenna(Room room, HabboItem antenna) { + if (room == null || antenna == null || antenna.getBaseItem() == null) return; + if (antenna.getBaseItem().getStateCount() <= 1) return; + + final long token = System.currentTimeMillis(); + ANTENNA_PULSE_TOKENS.put(antenna.getId(), token); + + if ("1".equals(antenna.getExtradata())) { + antenna.setExtradata("0"); + room.updateItemState(antenna); + } + + antenna.setExtradata("1"); + room.updateItemState(antenna); + + Emulator.getThreading().run(() -> { + if (!room.isLoaded()) return; + + Long currentToken = ANTENNA_PULSE_TOKENS.get(antenna.getId()); + if (currentToken == null || currentToken.longValue() != token) return; + + antenna.setExtradata("0"); + room.updateItemState(antenna); + ANTENNA_PULSE_TOKENS.remove(antenna.getId(), token); + }, ANTENNA_PULSE_MS); + } + @Override public void serializeWiredData(ServerMessage message, Room room) { List itemsSnapshot = new ArrayList<>(this.items); @@ -161,6 +188,16 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { item.getRoomId() != this.getRoomId() || room.getHabboItem(item.getId()) == null); this.items.retainAll(itemsSnapshot); + List forwardSnapshot = new ArrayList<>(this.forwardItems); + forwardSnapshot.removeIf(item -> + item.getRoomId() != this.getRoomId() || room.getHabboItem(item.getId()) == null); + this.forwardItems.retainAll(forwardSnapshot); + + String forwardString = forwardSnapshot.stream() + .filter(Objects::nonNull) + .map(item -> Integer.toString(item.getId())) + .collect(Collectors.joining(";")); + message.appendBoolean(false); message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); message.appendInt(itemsSnapshot.size()); @@ -169,7 +206,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); - message.appendString(""); + message.appendString(forwardString); message.appendInt(6); message.appendInt(antennaSource); @@ -219,6 +256,12 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { newItems.add(it); } + for (HabboItem receiver : newItems) { + if (!isAntennaItem(receiver)) { + throw new WiredSaveException("Only antenna furni can be selected"); + } + } + if (room != null && room.getRoomSpecialTypes() != null) { for (HabboItem receiver : newItems) { int count = room.getRoomSpecialTypes().countSendersTargetingReceiver(receiver.getId(), this); @@ -234,18 +277,36 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } int[] params = settings.getIntParams(); - this.antennaSource = params.length > 0 ? params[0] : ANTENNA_PICKED; - this.furniForward = params.length > 1 ? params[1] : FORWARD_NONE; - this.userForward = params.length > 2 ? params[2] : FORWARD_NONE; + int requestedAntennaSource = params.length > 0 ? params[0] : ANTENNA_PICKED; + this.furniForward = normalizeSource(params.length > 1 ? params[1] : WiredSourceUtil.SOURCE_TRIGGER); + this.userForward = normalizeSource(params.length > 2 ? params[2] : WiredSourceUtil.SOURCE_TRIGGER); this.signalPerFurni = params.length > 3 && params[3] == 1; this.signalPerUser = params.length > 4 && params[4] == 1; this.channel = params.length > 5 ? params[5] : 0; + this.antennaSource = requestedAntennaSource; + if (!newItems.isEmpty()) { + this.antennaSource = newItems.get(0).getId(); + } + + List newForwardItems = new ArrayList<>(); + if (this.furniForward == WiredSourceUtil.SOURCE_SELECTED && room != null) { + newForwardItems = parseForwardItems(settings.getStringParam(), room); + } + if (newForwardItems.size() > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + this.items.clear(); this.items.addAll(newItems); + + this.forwardItems.clear(); + if (this.furniForward == WiredSourceUtil.SOURCE_SELECTED) { + this.forwardItems.addAll(newForwardItems); + } this.setDelay(delay); - LOGGER.debug("[SendSignal] saveData: antennaSource={}, furniForward={}, userForward={}, signalPerFurni={}, signalPerUser={}, channel={}, items={}", - antennaSource, furniForward, userForward, signalPerFurni, signalPerUser, channel, items.size()); + LOGGER.debug("[SendSignal] saveData: antennaSource={}, furniForward={}, userForward={}, signalPerFurni={}, signalPerUser={}, channel={}, items={}, forwardItems={}", + antennaSource, furniForward, userForward, signalPerFurni, signalPerUser, channel, items.size(), forwardItems.size()); return true; } @@ -259,9 +320,11 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { @Override public String getWiredData() { List itemsSnapshot = new ArrayList<>(this.items); + List forwardSnapshot = new ArrayList<>(this.forwardItems); return WiredManager.getGson().toJson(new JsonData( this.getDelay(), itemsSnapshot.stream().map(HabboItem::getId).collect(Collectors.toList()), + forwardSnapshot.stream().map(HabboItem::getId).collect(Collectors.toList()), antennaSource, furniForward, userForward, signalPerFurni, signalPerUser, channel )); } @@ -269,14 +332,15 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { this.items = new THashSet<>(); + this.forwardItems = new THashSet<>(); String wiredData = set.getString("wired_data"); if (wiredData != null && wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); this.antennaSource = data.antennaSource; - this.furniForward = data.furniForward; - this.userForward = data.userForward; + this.furniForward = normalizeSource(data.furniForward); + this.userForward = normalizeSource(data.userForward); this.signalPerFurni = data.signalPerFurni; this.signalPerUser = data.signalPerUser; this.channel = data.channel; @@ -286,21 +350,84 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { if (item != null) this.items.add(item); } } + if (data.forwardItemIds != null) { + for (Integer id : data.forwardItemIds) { + HabboItem item = room.getHabboItem(id); + if (item != null) this.forwardItems.add(item); + } + } + + if (this.antennaSource <= ANTENNA_TRIGGER && !this.items.isEmpty()) { + HabboItem first = this.items.iterator().next(); + if (first != null) this.antennaSource = first.getId(); + } } } @Override public void onPickUp() { this.items.clear(); + this.forwardItems.clear(); this.antennaSource = ANTENNA_PICKED; - this.furniForward = FORWARD_NONE; - this.userForward = FORWARD_NONE; + this.furniForward = WiredSourceUtil.SOURCE_TRIGGER; + this.userForward = WiredSourceUtil.SOURCE_TRIGGER; this.signalPerFurni = false; this.signalPerUser = false; this.channel = 0; this.setDelay(0); } + private int normalizeSource(int source) { + if (source == 1) return WiredSourceUtil.SOURCE_TRIGGER; + if (source == WiredSourceUtil.SOURCE_TRIGGER + || source == WiredSourceUtil.SOURCE_SELECTED + || source == WiredSourceUtil.SOURCE_SELECTOR + || source == WiredSourceUtil.SOURCE_SIGNAL) { + return source; + } + return WiredSourceUtil.SOURCE_TRIGGER; + } + + private List parseForwardItems(String data, Room room) throws WiredSaveException { + List results = new ArrayList<>(); + if (data == null || data.trim().isEmpty() || room == null) return results; + + Set seen = new HashSet<>(); + String[] parts = data.split(FORWARD_ITEM_SPLIT_REGEX); + + for (String part : parts) { + if (part == null) continue; + + String trimmed = part.trim(); + if (trimmed.isEmpty()) continue; + + int itemId; + try { + itemId = Integer.parseInt(trimmed); + } catch (NumberFormatException e) { + continue; + } + + if (itemId <= 0 || !seen.add(itemId)) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", itemId)); + + results.add(item); + } + + return results; + } + + private boolean isAntennaItem(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 normalized = interaction.toLowerCase(); + return normalized.equals(ANTENNA_INTERACTION); + } + @Override public WiredEffectType getType() { return type; @@ -328,6 +455,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { static class JsonData { int delay; List itemIds; + List forwardItemIds; int antennaSource; int furniForward; int userForward; @@ -335,10 +463,11 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { boolean signalPerUser; int channel; - public JsonData(int delay, List itemIds, int antennaSource, int furniForward, + public JsonData(int delay, List itemIds, List forwardItemIds, int antennaSource, int furniForward, int userForward, boolean signalPerFurni, boolean signalPerUser, int channel) { this.delay = delay; this.itemIds = itemIds; + this.forwardItemIds = forwardItemIds; this.antennaSource = antennaSource; this.furniForward = furniForward; this.userForward = userForward; 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 5edfed63..14bae162 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 @@ -112,17 +112,17 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { @Override public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { int[] params = settings.getIntParams(); - if (params == null || params.length < 1) { - throw new WiredSaveException("wf_slc_furni_bytype: intParams must have at least 1 element"); + if (params == null || params.length < 4) { + throw new WiredSaveException("wf_slc_furni_bytype: intParams must have at least 4 elements"); } - this.sourceType = params[0]; + this.sourceType = SOURCE_FURNI_PICKED; this.matchState = params.length > 1 && params[1] == 1; this.filterExisting = params.length > 2 && params[2] == 1; this.invert = params.length > 3 && params[3] == 1; this.pickedFurniIds = new ArrayList<>(); - if (this.sourceType == SOURCE_FURNI_PICKED && settings.getFurniIds() != null) { + if (settings.getFurniIds() != null) { for (int id : settings.getFurniIds()) { if (pickedFurniIds.size() >= MAX_PICKED_FURNI) break; pickedFurniIds.add(id); @@ -135,12 +135,10 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { @Override public void serializeWiredData(ServerMessage message, Room room) { - boolean pickMode = (sourceType == SOURCE_FURNI_PICKED); + message.appendBoolean(true); + message.appendInt(MAX_PICKED_FURNI); - message.appendBoolean(pickMode); - message.appendInt(pickMode ? MAX_PICKED_FURNI : 0); - - if (pickMode && !pickedFurniIds.isEmpty()) { + if (!pickedFurniIds.isEmpty()) { message.appendInt(pickedFurniIds.size()); pickedFurniIds.forEach(message::appendInt); } else { @@ -152,7 +150,7 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { message.appendString(""); message.appendInt(4); - message.appendInt(sourceType); + message.appendInt(SOURCE_FURNI_PICKED); message.appendInt(matchState ? 1 : 0); message.appendInt(filterExisting ? 1 : 0); message.appendInt(invert ? 1 : 0); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java index c2e96e93..13463c7b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java @@ -1,5 +1,6 @@ package com.eu.habbo.habbohotel.items.interactions.wired.triggers; +import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; @@ -11,27 +12,49 @@ import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredEvent; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveException; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; +import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; public class WiredTriggerReceiveSignal extends InteractionWiredTrigger { public static final WiredTriggerType type = WiredTriggerType.RECEIVE_SIGNAL; + private static final String ANTENNA_INTERACTION = "antenna"; + private static final long ACTIVATION_PULSE_MS = 300L; + private int channel = 0; // signal channel (0-based) + private THashSet items; + private final AtomicLong activationToken = new AtomicLong(); public WiredTriggerReceiveSignal(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); + this.items = new THashSet<>(); } public WiredTriggerReceiveSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); } @Override public boolean matches(HabboItem triggerItem, WiredEvent event) { - return event.getType() == WiredEvent.Type.SIGNAL_RECEIVED - && event.getSignalChannel() == this.channel; + if (event.getType() != WiredEvent.Type.SIGNAL_RECEIVED) return false; + + if (!this.items.isEmpty()) { + int signalChannel = event.getSignalChannel(); + for (HabboItem antenna : this.items) { + if (antenna != null && antenna.getId() == signalChannel) return true; + } + return false; + } + + return event.getSignalChannel() == this.channel; } public int getChannel() { @@ -59,14 +82,33 @@ public class WiredTriggerReceiveSignal extends InteractionWiredTrigger { int senderCount = 0; try { if (room != null && room.getRoomSpecialTypes() != null) { - senderCount = room.getRoomSpecialTypes().countSendersTargetingReceiver(this.getId()); + if (!this.items.isEmpty()) { + for (HabboItem item : this.items) { + senderCount += room.getRoomSpecialTypes().countSendersTargetingReceiver(item.getId()); + } + } else { + senderCount = room.getRoomSpecialTypes().countSendersTargetingReceiver(this.getId()); + } } } catch (Exception e) { } + THashSet itemsToRemove = new THashSet<>(); + for (HabboItem item : this.items) { + if (item.getRoomId() != this.getRoomId() || room.getHabboItem(item.getId()) == null) { + itemsToRemove.add(item); + } + } + for (HabboItem item : itemsToRemove) { + this.items.remove(item); + } + message.appendBoolean(false); - message.appendInt(0); - message.appendInt(0); + 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(""); @@ -82,37 +124,100 @@ public class WiredTriggerReceiveSignal extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { + this.items.clear(); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + int count = settings.getFurniIds().length; + + for (int i = 0; i < count; i++) { + HabboItem item = room.getHabboItem(settings.getFurniIds()[i]); + if (item == null) continue; + if (!isAntennaItem(item)) throw new WiredTriggerSaveException("wiredfurni.error.require_antenna_furni"); + this.items.add(item); + } + int[] params = settings.getIntParams(); this.channel = params.length > 0 ? params[0] : 0; return true; } + @Override + public void activateBox(Room room, RoomUnit roomUnit, long millis) { + if (roomUnit != null) { + this.addUserExecutionCache(roomUnit.getId(), millis); + } + + if (room == null || room.isHideWired() || this.getBaseItem().getStateCount() <= 1) { + return; + } + + final long token = this.activationToken.incrementAndGet(); + + if ("1".equals(this.getExtradata())) { + this.setExtradata("0"); + room.sendComposer(new ItemStateComposer(this).compose()); + } + + this.setExtradata("1"); + room.sendComposer(new ItemStateComposer(this).compose()); + + Emulator.getThreading().run(() -> { + if (!room.isLoaded()) return; + if (this.activationToken.get() != token) return; + + this.setExtradata("0"); + room.sendComposer(new ItemStateComposer(this).compose()); + }, ACTIVATION_PULSE_MS); + } + @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(channel)); + return WiredManager.getGson().toJson(new JsonData( + channel, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items = new THashSet<>(); String wiredData = set.getString("wired_data"); if (wiredData != null && wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.channel = data.channel; + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + if (item != null) this.items.add(item); + } + } } } @Override public void onPickUp() { this.channel = 0; + this.items.clear(); + } + + private boolean isAntennaItem(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 normalized = interaction.toLowerCase(); + return normalized.equals(ANTENNA_INTERACTION); } static class JsonData { int channel; + List itemIds; public JsonData() {} - public JsonData(int channel) { + public JsonData(int channel, List itemIds) { this.channel = channel; + this.itemIds = itemIds; } } } 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 28300ca6..0cc66859 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 @@ -214,24 +214,16 @@ public final class WiredEngine { // Initial step for trigger state.step(); - // Activate the trigger box animation - if (stack.triggerItem() instanceof InteractionWiredTrigger) { - InteractionWiredTrigger trigger = (InteractionWiredTrigger) stack.triggerItem(); - trigger.activateBox(room, event.getActor().orElse(null), currentTime); - } - debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", event.getType(), stack.triggerItem() != null ? stack.triggerItem().getId() : "null", stack.conditions().size(), stack.effects().size()); - - // Activate extras (for their animation) - activateExtras(room, stack.triggerItem(), event.getActor().orElse(null), currentTime); // Run selectors before conditions so targets are available + List executedSelectors = Collections.emptyList(); if (stack.hasEffects()) { - executeSelectors(stack, ctx, currentTime); + executedSelectors = executeSelectors(stack, ctx); } // Evaluate conditions @@ -253,6 +245,17 @@ public final class WiredEngine { return false; } + RoomUnit actor = event.getActor().orElse(null); + + // Only show the trigger/selector activation when the stack is actually allowed to continue. + if (stack.triggerItem() instanceof InteractionWiredTrigger) { + InteractionWiredTrigger trigger = (InteractionWiredTrigger) stack.triggerItem(); + trigger.activateBox(room, actor, currentTime); + } + + activateExtras(room, stack.triggerItem(), actor, currentTime); + finalizeSelectors(executedSelectors, ctx, currentTime); + // Execute effects if (stack.hasEffects()) { executeEffects(stack, ctx, currentTime); @@ -420,9 +423,11 @@ public final class WiredEngine { /** * Execute selector effects before conditions so ctx.targets() is populated. */ - private void executeSelectors(WiredStack stack, WiredContext ctx, long currentTime) { + private List executeSelectors(WiredStack stack, WiredContext ctx) { List effects = stack.effects(); - if (effects.isEmpty()) return; + if (effects.isEmpty()) return Collections.emptyList(); + + List executedSelectors = new ArrayList<>(); for (IWiredEffect effect : effects) { if (!effect.isSelector()) continue; @@ -433,16 +438,29 @@ public final class WiredEngine { ctx.state().step(); try { effect.execute(ctx); - if (effect instanceof InteractionWiredEffect) { - InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; - wiredEffect.setCooldown(currentTime); - wiredEffect.activateBox(ctx.room(), ctx.actor().orElse(null), currentTime); + executedSelectors.add((InteractionWiredEffect) effect); } } catch (Exception e) { LOGGER.warn("Error executing selector: {}", e.getMessage()); } } + + return executedSelectors; + } + + private void finalizeSelectors(List executedSelectors, WiredContext ctx, long currentTime) { + if (executedSelectors == null || executedSelectors.isEmpty()) { + return; + } + + Room room = ctx.room(); + RoomUnit actor = ctx.actor().orElse(null); + + for (InteractionWiredEffect wiredEffect : executedSelectors) { + wiredEffect.setCooldown(currentTime); + wiredEffect.activateBox(room, actor, currentTime); + } } /** diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java index 83da31e5..bf27dc0e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java @@ -34,17 +34,21 @@ public class WiredTriggerSaveDataEvent extends MessageHandler { if (saveMethod.get().getParameterTypes()[0] == WiredSettings.class) { WiredSettings settings = InteractionWired.readSettings(this.packet, false); - if (trigger.saveData(settings)) { - this.client.sendResponse(new WiredSavedComposer()); + try { + if (trigger.saveData(settings)) { + this.client.sendResponse(new WiredSavedComposer()); - trigger.needsUpdate(true); + trigger.needsUpdate(true); - Emulator.getThreading().run(trigger); - - // Invalidate wired cache when trigger is saved - WiredManager.invalidateRoom(room); - } else { - this.client.sendResponse(new UpdateFailedComposer("There was an error while saving that trigger")); + Emulator.getThreading().run(trigger); + + // Invalidate wired cache when trigger is saved + WiredManager.invalidateRoom(room); + } else { + this.client.sendResponse(new UpdateFailedComposer("There was an error while saving that trigger")); + } + } catch (WiredTriggerSaveException e) { + this.client.sendResponse(new UpdateFailedComposer(e.getMessage())); } } else { if ((boolean) saveMethod.get().invoke(trigger, this.packet)) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveException.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveException.java new file mode 100644 index 00000000..63fc33a2 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveException.java @@ -0,0 +1,7 @@ +package com.eu.habbo.messages.incoming.wired; + +public class WiredTriggerSaveException extends RuntimeException { + public WiredTriggerSaveException(String message) { + super(message); + } +}