From 8a9670759af7cbd913d8ea1f2bfc78f337d27bce Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 18 Mar 2026 14:38:21 +0100 Subject: [PATCH] feat(wired): add altitude and relative move effects --- .../habbo/habbohotel/items/ItemManager.java | 2 + .../effects/WiredEffectRelativeMove.java | 283 +++++++++++++++++ .../wired/effects/WiredEffectSetAltitude.java | 288 ++++++++++++++++++ .../habbohotel/wired/WiredEffectType.java | 4 +- 4 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRelativeMove.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.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 2a944f95..c23f37d6 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 @@ -256,6 +256,8 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_act_alert", WiredEffectAlert.class)); this.interactionsList.add(new ItemInteraction("wf_act_give_handitem", WiredEffectGiveHandItem.class)); this.interactionsList.add(new ItemInteraction("wf_act_give_effect", WiredEffectGiveEffect.class)); + this.interactionsList.add(new ItemInteraction("wf_act_set_altitude", WiredEffectSetAltitude.class)); + this.interactionsList.add(new ItemInteraction("wf_act_rel_mov", WiredEffectRelativeMove.class)); this.interactionsList.add(new ItemInteraction("wf_slc_furni_area", WiredEffectFurniArea.class)); this.interactionsList.add(new ItemInteraction("wf_slc_furni_neighborhood", WiredEffectFurniNeighborhood.class)); this.interactionsList.add(new ItemInteraction("wf_slc_furni_bytype", WiredEffectFurniByType.class)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRelativeMove.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRelativeMove.java new file mode 100644 index 00000000..e2fcf249 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRelativeMove.java @@ -0,0 +1,283 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +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.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredEffectRelativeMove extends InteractionWiredEffect { + private static final int HORIZONTAL_NEGATIVE = 0; + private static final int HORIZONTAL_POSITIVE = 1; + private static final int VERTICAL_NEGATIVE = 0; + private static final int VERTICAL_POSITIVE = 1; + private static final int MAX_DISTANCE = 20; + + public static final WiredEffectType type = WiredEffectType.RELATIVE_MOVE; + + private final List items = new ArrayList<>(); + private int horizontalDirection = HORIZONTAL_POSITIVE; + private int horizontalDistance = 0; + private int verticalDirection = VERTICAL_POSITIVE; + private int verticalDistance = 0; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectRelativeMove(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectRelativeMove(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null || room.getLayout() == null) { + return; + } + + List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.items.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + } + + int deltaX = this.getHorizontalOffset(); + int deltaY = this.getVerticalOffset(); + + if (deltaX == 0 && deltaY == 0) { + return; + } + + for (HabboItem item : effectiveItems) { + if (item == null || item.getRoomId() != this.getRoomId()) { + continue; + } + + short targetX = (short) (item.getX() + deltaX); + short targetY = (short) (item.getY() + deltaY); + + RoomTile targetTile = room.getLayout().getTile(targetX, targetY); + if (targetTile == null) { + continue; + } + + room.moveFurniTo(item, targetTile, item.getRotation(), null, true, false); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.horizontalDirection, + this.horizontalDistance, + this.verticalDirection, + this.verticalDistance, + this.furniSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.horizontalDirection = this.normalizeBinary(data.horizontalDirection, HORIZONTAL_POSITIVE); + this.horizontalDistance = this.normalizeDistance(data.horizontalDistance); + this.verticalDirection = this.normalizeBinary(data.verticalDirection, VERTICAL_POSITIVE); + this.verticalDistance = this.normalizeDistance(data.verticalDistance); + this.furniSource = data.furniSource; + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + return; + } + + this.horizontalDirection = HORIZONTAL_POSITIVE; + this.horizontalDistance = 0; + this.verticalDirection = VERTICAL_POSITIVE; + this.verticalDistance = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.horizontalDirection = HORIZONTAL_POSITIVE; + this.horizontalDistance = 0; + this.verticalDirection = VERTICAL_POSITIVE; + this.verticalDistance = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(5); + message.appendInt(this.horizontalDirection); + message.appendInt(this.horizontalDistance); + message.appendInt(this.verticalDirection); + message.appendInt(this.verticalDistance); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + + if (params.length < 5) { + throw new WiredSaveException("Invalid data"); + } + + this.horizontalDirection = this.normalizeBinary(params[0], HORIZONTAL_POSITIVE); + this.horizontalDistance = this.normalizeDistance(params[1]); + this.verticalDirection = this.normalizeBinary(params[2], VERTICAL_POSITIVE); + this.verticalDistance = this.normalizeDistance(params[3]); + this.furniSource = params[4]; + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + List newItems = new ArrayList<>(); + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + newItems.add(item); + } + + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + private int getHorizontalOffset() { + if (this.horizontalDistance <= 0) { + return 0; + } + + return (this.horizontalDirection == HORIZONTAL_NEGATIVE) ? -this.horizontalDistance : this.horizontalDistance; + } + + private int getVerticalOffset() { + if (this.verticalDistance <= 0) { + return 0; + } + + return (this.verticalDirection == VERTICAL_NEGATIVE) ? -this.verticalDistance : this.verticalDistance; + } + + private int normalizeBinary(int value, int fallback) { + if (value == 0 || value == 1) { + return value; + } + + return fallback; + } + + private int normalizeDistance(int value) { + return Math.max(0, Math.min(MAX_DISTANCE, value)); + } + + static class JsonData { + int delay; + List itemIds; + int horizontalDirection; + int horizontalDistance; + int verticalDirection; + int verticalDistance; + int furniSource; + + public JsonData(int delay, List itemIds, int horizontalDirection, int horizontalDistance, int verticalDirection, int verticalDistance, int furniSource) { + this.delay = delay; + this.itemIds = itemIds; + this.horizontalDirection = horizontalDirection; + this.horizontalDistance = horizontalDistance; + this.verticalDirection = verticalDirection; + this.verticalDistance = verticalDistance; + this.furniSource = furniSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java new file mode 100644 index 00000000..e43a4a68 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java @@ -0,0 +1,288 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +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.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class WiredEffectSetAltitude extends InteractionWiredEffect { + private static final Pattern ALTITUDE_PATTERN = Pattern.compile("^\\d+(\\.\\d{1,2})?$"); + + private static final int OPERATOR_INCREASE = 0; + private static final int OPERATOR_DECREASE = 1; + private static final int OPERATOR_SET = 2; + + public static final WiredEffectType type = WiredEffectType.SET_ALTITUDE; + + private final List items = new ArrayList<>(); + private int operator = OPERATOR_SET; + private double altitude = 0.0D; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectSetAltitude(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectSetAltitude(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.items.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + } + + for (HabboItem item : effectiveItems) { + if (item == null || item.getRoomId() != this.getRoomId()) { + continue; + } + + RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); + if (tile == null) { + continue; + } + + double nextAltitude = this.computeAltitude(item.getZ()); + room.moveFurniTo(item, tile, item.getRotation(), nextAltitude, null, true, false); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.operator, + this.formatAltitude(this.altitude), + this.furniSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.operator = this.normalizeOperator(data.operator); + this.altitude = this.parseAltitudeOrDefault(data.altitude); + this.furniSource = data.furniSource; + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + return; + } + + this.operator = OPERATOR_SET; + this.altitude = 0.0D; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.operator = OPERATOR_SET; + this.altitude = 0.0D; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.formatAltitude(this.altitude)); + message.appendInt(2); + message.appendInt(this.operator); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + this.operator = (params.length > 0) ? this.normalizeOperator(params[0]) : OPERATOR_SET; + this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + List newItems = new ArrayList<>(); + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + newItems.add(item); + } + + this.altitude = this.parseAltitude(settings.getStringParam()); + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + private int normalizeOperator(int value) { + if (value < OPERATOR_INCREASE || value > OPERATOR_SET) { + return OPERATOR_SET; + } + + return value; + } + + private double computeAltitude(double currentAltitude) { + double nextAltitude; + + switch (this.operator) { + case OPERATOR_INCREASE: + nextAltitude = currentAltitude + this.altitude; + break; + case OPERATOR_DECREASE: + nextAltitude = currentAltitude - this.altitude; + break; + case OPERATOR_SET: + default: + nextAltitude = this.altitude; + break; + } + + return this.normalizeAltitude(nextAltitude); + } + + private double parseAltitude(String value) throws WiredSaveException { + String normalized = (value != null) ? value.trim() : ""; + + if (normalized.isEmpty()) { + return 0.0D; + } + + if (!ALTITUDE_PATTERN.matcher(normalized).matches()) { + throw new WiredSaveException("Invalid altitude value"); + } + + try { + return this.normalizeAltitude(new BigDecimal(normalized).doubleValue()); + } catch (NumberFormatException exception) { + throw new WiredSaveException("Invalid altitude value"); + } + } + + private double parseAltitudeOrDefault(String value) { + try { + return this.parseAltitude(value); + } catch (WiredSaveException exception) { + return 0.0D; + } + } + + private double normalizeAltitude(double value) { + double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value)); + return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + private String formatAltitude(double value) { + BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros(); + return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString(); + } + + static class JsonData { + int delay; + List itemIds; + int operator; + String altitude; + int furniSource; + + public JsonData(int delay, List itemIds, int operator, String altitude, int furniSource) { + this.delay = delay; + this.itemIds = itemIds; + this.operator = operator; + this.altitude = altitude; + this.furniSource = furniSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java index 0d5e74d2..a692788f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java @@ -32,7 +32,9 @@ public enum WiredEffectType { FURNI_BYTYPE_SELECTOR(30), USERS_AREA_SELECTOR(31), USERS_NEIGHBORHOOD_SELECTOR(32), - SEND_SIGNAL(33); + SEND_SIGNAL(33), + SET_ALTITUDE(39), + RELATIVE_MOVE(40); public final int code;